2 votes

Conditions combinatoires complexes sur la programmation dynamique

J'étudie comment une approche de conception par programmation dynamique est liée aux propriétés combinatoires sous-jacentes des problèmes.

Pour cela, je me penche sur l'instance canonique des problème de changement de pièces : Soit S = [d_1, d_2, ..., d_m] y n > 0 être un montant demandé. De combien de façons pouvons-nous additionner jusqu'à n en n'utilisant rien d'autre que les éléments de S ?

Si nous suivons un Programmation dynamique Pour concevoir un algorithme permettant de résoudre ce problème avec une complexité polynomiale, nous commencerions par examiner le problème et la façon dont il est lié à des sous-problèmes plus petits et plus simples. Cela permettrait d'obtenir un relation récursive décrivant un pas inductif représentant le problème en termes de solutions à ses sous-problèmes connexes. On peut alors mettre en œuvre soit un mémorisation technique ou une tabulation pour mettre en œuvre efficacement cette relation récursive de manière descendante ou ascendante, respectivement.

Une relation récursive pour résoudre cette instance du problème pourrait être la suivante (syntaxe Python 3.6 et indexation basée sur 0) :

def C(S, m, n):
    if n < 0:
        return 0
    if n == 0:
        return 1
    if m <= 0:
        return 0
    count_wout_high_coin = C(S, m - 1, n)
    count_with_high_coin = C(S, m, n - S[m - 1])
    return count_wout_high_coin + count_with_high_coin

Cette relation récursive donne un nombre correct de solutions mais sans tenir compte de l'ordre. Cependant, cette relation :

def C(S, n):
  if n < 0:
    return 0
  if n == 0:
    return 1
  return sum([C(S, n - coin) for coin in S])

produit une quantité correcte de solutions tout en respectant la commande.

Je m'intéresse à la capture de motifs combinatoires plus subtils par le biais d'une relation de récursion qui peut être optimisée par la mémorisation/tabulation.

Par exemple, cette relation :

def C(S, m, n, p):
    if n < 0:
        return 0
    if n == 0 and not p:
        return 1
    if n == 0 and p:
        return 0
    if m == 0:
        return 0
    return C(S, m - 1, n, p) + C(S, m, n - S[n - 1], not p)

donne une solution qui ne tient pas compte de l'ordre mais qui ne compte que les solutions ayant un nombre pair de sommets. La même relation peut être modifiée pour tenir compte de l'ordre et compter le nombre pair de sommets :

def C(S, n, p):
    if n < 0:
        return 0
    if n == 0 and not p:
        return 1
    if n == 0 and p:
        return 0
    return sum([C(S, n - coin, not p) for coin in S])

Cependant, que se passe-t-il si nous avons plus d'une personne entre lesquelles nous voulons partager les pièces ? Disons que je veux partager n entre 2 personnes, à condition que chaque personne reçoive le même nombre de pièces, quelle que soit la somme totale que chacune reçoit. Sur les 14 solutions, seules 7 comprennent un nombre pair de pièces, de sorte que je puisse les répartir équitablement. Mais je veux exclure les attributions redondantes de pièces à chaque personne. Par exemple, 1 + 2 + 2 + 1 y 1 + 2 + 1 + 2 sont des solutions différentes lorsque l'ordre est important, MAIS elles représentent la même répartition des pièces entre deux personnes, à savoir la personne B obtiendrait 1 + 2 = 2 + 1 . J'ai du mal à trouver une récursion pour compter les splits de manière non redondante.

1voto

גלעד ברקן Points 3044

(Avant de développer une réponse possible, permettez-moi de souligner que le fait de compter les parts de l'échange de pièces, pour même n , par somme plutôt que de compter les pièces serait plus ou moins trivial puisque nous pouvons compter le nombre de façons d'échanger n / 2 et le multiplier par lui-même :)

Maintenant, si vous voulez compter les divisions de l'échange de pièces en fonction de nombre de pièces et d'exclure les attributions redondantes de pièces à chaque personne (par exemple, lorsque la division de 1 + 2 + 2 + 1 en deux parties de taille égale est seulement soit (1,1) | (2,2) , (2,2) | (1,1) o (1,2) | (1,2) et l'ordre des éléments dans chaque partie n'a pas d'importance), nous pourrions nous appuyer sur votre première énumération de partitions où l'ordre n'est pas pris en compte.

Cependant, nous aurions besoin de connaître le multiset d'éléments de chaque partition (ou un agrégat d'éléments similaires) afin de compter les possibilités de les diviser en deux. Par exemple, pour compter les possibilités de diviser 1 + 2 + 2 + 1 nous devons d'abord compter combien de pièces nous avons de chaque côté :

def partitions_with_even_number_of_parts_as_multiset(n, coins):
  results = []

  def C(m, n, s, p):
    if n < 0 or m <= 0:
      return

    if n == 0:
      if not p:
        results.append(s)
      return

    C(m - 1, n, s, p)

    _s = s[:]
    _s[m - 1] += 1

    C(m, n - coins[m - 1], _s, not p)

  C(len(coins), n, [0] * len(coins), False)

  return results

Sortie :

=> partitions_with_even_number_of_parts_as_multiset(6, [1,2,6])
=> [[6, 0, 0], [2, 2, 0]]
                ^ ^ ^ ^ this one represents two 1's and two 2's

Maintenant, puisque nous comptons les façons de choisir la moitié d'entre elles, nous devons trouver le coefficient de x^2 dans la multiplication polynomiale

(x^2 + x + 1) * (x^2 + x + 1) = ... 3x^2 ...

qui représente les trois façons de choisir deux parmi les multisets comptés [2,2] :

2,0 => 1,1
0,2 => 2,2
1,1 => 1,2

En Python, nous pouvons utiliser numpy.polymul pour multiplier les coefficients du polynôme. Ensuite, nous recherchons le coefficient approprié dans le résultat.

Par exemple :

import numpy    

def count_split_partitions_by_multiset_count(multiset):
  coefficients = (multiset[0] + 1) * [1]

  for i in xrange(1, len(multiset)):
    coefficients = numpy.polymul(coefficients, (multiset[i] + 1) * [1])

  return coefficients[ sum(multiset) / 2 ]

Sortie :

=> count_split_partitions_by_multiset_count([2,2,0])
=> 3

1voto

גלעד ברקן Points 3044

Voici un tableau de mise en œuvre et un petit développement sur La belle réponse d'algrid . Cela produit une réponse pour f(500, [1, 2, 6, 12, 24, 48, 60]) en 2 secondes environ.

La simple déclaration de C(n, k, S) = sum(C(n - s_i, k - 1, S[i:])) signifie additionner toutes les façons d'arriver à la somme actuelle, n en utilisant k pièces. Alors si on divise n de toutes les façons dont il peut être divisé en deux, nous pouvons simplement ajouter toutes les façons dont chacune de ces parties peut être faite à partir du même nombre, k de pièces de monnaie.

La beauté de la fixation du sous-ensemble de pièces dans lequel nous choisissons une liste décroissante signifie que toute combinaison arbitraire de pièces ne sera comptée qu'une seule fois - elle sera comptée dans le calcul où la pièce la plus à gauche de la combinaison est la première pièce de notre sous-ensemble décroissant (en supposant que nous les ordonnions de la même manière). Par exemple, le sous-ensemble arbitraire [6, 24, 48] , tiré de [1, 2, 6, 12, 24, 48, 60] ne serait compté dans la somme que pour le sous-ensemble [6, 12, 24, 48, 60] depuis le prochain sous-ensemble, [12, 24, 48, 60] ne comprendrait pas 6 et le sous-ensemble précédent [2, 6, 12, 24, 48, 60] a au moins un 2 pièce.

Le code Python (voir aquí ; confirmer aquí ):

import time

def f(n, coins):
  t0 = time.time()

  min_coins = min(coins)

  m = [[[0] * len(coins) for k in xrange(n / min_coins + 1)] for _n in xrange(n + 1)]

  # Initialize base case
  for i in xrange(len(coins)):
    m[0][0][i] = 1

  for i in xrange(len(coins)):
    for _i in xrange(i + 1):
      for _n in xrange(coins[_i], n + 1):
        for k in xrange(1, _n / min_coins + 1):
          m[_n][k][i] += m[_n - coins[_i]][k - 1][_i]

  result = 0

  for a in xrange(1, n + 1):
    b = n - a

    for k in xrange(1, n / min_coins + 1):
      result = result + m[a][k][len(coins) - 1] * m[b][k][len(coins) - 1]

  total_time = time.time() - t0

  return (result, total_time)

print f(500, [1, 2, 6, 12, 24, 48, 60])

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X