Introduction aux notes d'étude des algorithmes Chapitre 4 Stratégie de division pour la conquête

Dans la stratégie diviser-pour-conquérir, nous résolvons un problème de manière récursive, et les trois étapes suivantes sont appliquées à chaque niveau de récursivité:
1. Décomposition. Divisez le problème en quelques sous-problèmes, la forme des sous-problèmes est la même que le problème d'origine, mais à une plus petite échelle.
2. Résolvez. Résolvez les sous-problèmes de manière récursive. Si la taille des sous-problèmes est suffisamment petite, arrêtez la récursivité et résolvez directement.
3. Fusionner. Combinez les solutions des sous-problèmes dans la solution du problème d'origine.

Lorsque le sous-problème est suffisamment grand pour être résolu de manière récursive, nous l'appelons un cas récursif. Lorsque le sous-problème devient suffisamment petit pour que la récursion ne soit plus nécessaire, nous disons que la récursivité a atteint son point bas et est entrée dans la situation de base.

Les expressions récursives peuvent prendre plusieurs formes. Un algorithme récursif peut diviser le problème en sous-problèmes de tailles variables, comme la division de 1/3 et 2/3. Et la taille du sous-problème ne doit pas nécessairement être une proportion fixe du problème d'origine.

Dans les applications pratiques, nous ignorerons certains détails techniques de la déclaration et de la solution récursives, comme l'appel de MERGE-SORT sur n éléments. Lorsque n est un nombre impair, les échelles des deux sous-problèmes sont n / 2 arrondies vers le haut et vers le bas, respectivement Integer, pas exactement n / 2. Les conditions aux limites sont un autre type de détails qui sont généralement négligés. Lorsque n est suffisamment petit, le temps d'exécution de l'algorithme est θ (1), mais nous ne modifions pas la formule récursive selon laquelle n est suffisamment petit pour trouver T (n), car ces les changements ne dépasseront pas un facteur constant, donc l'ordre de croissance de la fonction ne changera pas.

Investir dans des actions ne peut être acheté et vendu qu'une seule fois, afin de maximiser les rendements, c'est-à-dire acheter au prix le plus bas et vendre au prix le plus élevé, mais le prix le plus bas peut apparaître après le prix le plus élevé
:! [Insérer la description de l'image ici] (https: // img- blog.csdnimg.cn/20210320124004302.png?x-oss- process = image / watermark, type_ZmFuZ3poZW5naGVpdGk, shadow_10, text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R
au prix le plus élevé, vous pouvez vendre au plus haut prix le plus élevé ou au prix le plus bas que vous pensez Vous pouvez maximiser les revenus. Si cette stratégie est efficace, il est très simple de déterminer le revenu maximum: recherchez le prix le plus élevé et le prix le plus bas, puis commencez par le prix le plus élevé pour la gauche pour trouver le prix le plus bas sur la gauche, et commencez par le prix le plus bas vers la droite pour trouver le droit Le prix le plus élevé de, puis prenez les deux paires de prix avec la plus grande différence, mais le chiffre suivant est un contre- exemple: le
Insérez la description de l'image ici
problème ci - dessus peut être résolu simplement par la violence, essayez toutes les combinaisons possibles de dates d'achat et de vente, tant que la date de vente est à la date d'achat après la canette, un total de n(n-1)/2types de combinaisons de dates de n jours , et le le temps de traitement passé pour chaque date est au moins constant, par conséquent, cette méthode de temps de fonctionnement Ω (n²).

Nous pouvons examiner les variations de prix quotidiennes. La variation de prix le i-ième jour est définie comme la différence de prix entre le i-ème jour et le i-1 jour. Ensuite, le problème est de trouver le plus grand sous-tableau continu non vide. , appelé ce sous-tableau Le tableau est le plus grand sous-tableau: le
Insérez la description de l'image ici
plus grand problème de sous-tableau n'est significatif que lorsque le tableau contient des nombres négatifs. Si tous les éléments du tableau ne sont pas négatifs, le plus grand sous-tableau est la somme de l'ensemble déployer.

Pensez à utiliser la technologie Divide-and-Conquer pour résoudre le plus grand problème de sous-tableau. L'utilisation de la technologie Divide-and-Conquer signifie que nous devons diviser le sous-tableau en deux sous-tableaux de la même taille que possible, c'est-à-dire trouver la position centrale au milieu du sous-tableau, puis envisagez de résoudre les deux sous-tableaux A [bas… milieu] et A [milieu + 1… haut], la position de tout sous-tableau continu A [i… j] de A [bas… haut] doit être l'une des trois situations suivantes:
1. Entièrement situé dans le sous-tableau Dans A [bas… moyen], à ce moment bas <= i <= j <= moyen.
2. Il est complètement situé dans le sous-tableau A [mid + 1… high], à ce moment mid <i <= j <= high.
3. Traversez le point médian, à ce moment bas <= i <= moyen <= j <= haut.

En fait, un plus grand sous-tableau de A [bas… haut] doit être complètement situé dans A [bas… moyen], complètement situé dans A [milieu + 1… haut], et le plus grand de tous les sous-tableaux au milieu , nous Il est possible de résoudre récursivement les plus grands sous-tableaux qui sont complètement situés des deux côtés, donc tout le travail restant est de trouver le plus grand sous-tableau qui couvre le point médian, puis de choisir la plus grande somme dans ces trois cas:
Insérez la description de l'image ici
recherchez le plus grand sous-tableau qui couvre le point médian:

FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
	left-sum = -∞
	sum = 0
	for i = mid down to low
		sum = sum + A[i]
		if sum > left-sum
			left-sum = sum
			max-left = i
	right-sum = -∞
	sum = 0
	for j = mid + 1 to high
		sum = sum + A[j]
		if sum > right-sum
			right-sum = sum
			max-right = j
	return (max-left, max-right, left-sum + right-sum)

Le processus ci-dessus prend θ (n) temps.

Avec le pseudo-code de temps linéaire ci-dessus, vous pouvez écrire le pseudo-code suivant de l'algorithme de division et de conquête pour résoudre le plus grand problème de sous-matrice:

FIND-MAXIMUM-SUBARRAY(A, low, high)
	if high == low    // base case: only one element
		return (low, high, A[low])
	else 
		mid = floor((low + high) / 2)    // 向下取整
		(left-low, left-high, left-sum) = FIND-MAXIMUM-SUBARRAY(A, low, mid)
		(right-low, right-high, right-sum) = FIND-MAXIMUM-SUBARRAY(A, mid + 1, high)
		(cross-low, cross-high, cross-sum) = FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
		if left-sum >= right-sum and left-sum >= cross-sum
			return (left-low, left-high, left-sum)
		elseif right-sum >= left-sum and right-sum >= cross-sum
			return (right-low, right-high, right-sum)
		else 
			return (cross-low, cross-high, cross-sum)

La complexité temporelle de cette solution est θ (nlgn).

Implémentation C ++ du processus ci-dessus:

#include <iostream>
#include <vector>
using namespace std;

int findMaxCrossSubarray(const vector<int> &nums, size_t start, size_t mid, size_t end) {
    
    
	int leftSum = 0;
	int sum = 0;
	for (size_t i = mid + 1; i > start; --i) {
    
    
		sum += nums[i - 1];
		if (sum > leftSum) {
    
    
			leftSum = sum;
		}
	}

	int rightSum = 0;
	sum = 0;
	for (size_t i = mid + 1; i <= end; ++i) {
    
    
		sum += nums[i];
		if (sum > rightSum) {
    
    
			rightSum = sum;
		}
	}

	return leftSum + rightSum;
}

int findMaximumSubarray(const vector<int>& nums, size_t start, size_t end) {
    
    
	if (start == end) {
    
    
		return nums[start];
	}

	size_t mid = (start + end) >> 1;
	int leftMax = findMaximumSubarray(nums, start, mid);
	int rightMax = findMaximumSubarray(nums, mid + 1, end);
	int crossMax = findMaxCrossSubarray(nums, start, mid, end);

	return max(leftMax, max(rightMax, crossMax));
}

int main() {
    
    
	vector<int> ivec = {
    
     13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7 };
	cout << findMaximumSubarray(ivec, 0, ivec.size() - 1);
}

Trouvez le plus grand sous-tableau de manière non récursive et linéaire, et la complexité temporelle est O (n):

#include <iostream>
#include <vector>
#include <limits>
using namespace std;

int findMaximumSubarray(const vector<int>& nums, size_t start, size_t end) {
    
    
	int sumMax = numeric_limits<int>::min();
	int curSum = 0;
	for (size_t i = 0; i < nums.size(); ++i) {
    
    
		curSum += nums[i];
		if (curSum > sumMax) {
    
    
			sumMax = curSum;
		}

		if (curSum < 0) {
    
    
			curSum = 0;
		}
	}
	return sumMax;
}

int main() {
    
    
	vector<int> ivec = {
    
     13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7 };
	cout << findMaximumSubarray(ivec, 0, ivec.size() - 1);
}

Pseudo-code de multiplication de matrice:

SQUARE-MATRIX-MULTIPLY(A, B)
	n = A.rows
	let C be a new nXn matrix
	for i = 1 to n
		for j = 1 to n
			cij = 0
				for k = 1 to n
					cij = cij + aik * bkj
	return C

La multiplication matricielle ne prend pas nécessairement Ω (n³) de temps. Même la définition naturelle de la multiplication matricielle nécessite autant de multiplications scalaires. Il existe l'algorithme récursif de multiplication matricielle de Strassen, qui a une complexité temporelle de O (n 2,81 ).

Par souci de simplicité, on suppose que la matrice est nXn, où n est une puissance de 2, puis diviser la matrice nXn en 4 sous-matrices de n / 2XN / 2 Les propriétés d'une opération de matrice sont comme suit:. La
Insérez la description de l'image ici
suivante formule est équivalente à la formule ci-dessus:
Insérez la description de l'image ici
selon ce qui précède Le processus peut écrire un pseudo code:

SQUARE-MATRIX-MULTIPLY-RECURSIVE(A, B)
	n = A.rows
	let C be a new nXn matrix
	if n == 1
		c11 = a11 * b11
	else
		C11 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A11, B11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A12, B21)
		C12 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A11, B12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A12, B22)
		C21 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A21, B11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A22, B21)
		C22 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A21, B12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A22, B22)
	return C

Le code ci-dessus cache un détail subtil mais important sur la façon de décomposer la matrice. Si nous créons vraiment 12 nouvelles matrices n / 2Xn / 2, il faudra θ (n²) temps pour copier les éléments de la matrice. En fait, nous pouvons passer le indice Pour spécifier une sous-matrice.

Dans le processus ci-dessus, la cinquième ligne prend le temps de θ (1) pour calculer la matrice de décomposition, puis SQUARE-MATRIX-MULTIPLY-RECURSIVE est appelée huit fois. Chaque appel complète la multiplication de deux matrices n / 2Xn / 2. le temps total passé est de 8T (N / 2). Dans ce processus, il faut du temps θ (n²) pour appeler l'addition de matrice 4 fois, donc le temps total du cas récursif est:
Insérez la description de l'image ici
si la décomposition de la matrice est réalisée en copiant les éléments , le coût requis est θ (n²), le temps de fonctionnement total sera augmenté d'un facteur constant et T (n) restera inchangé. Par conséquent, la formule de temps d'exécution est la suivante:
Insérez la description de l'image ici
Cette méthode T (n) = θ (n³), donc l'algorithme simple de division et de conquête n'est pas meilleur que la méthode directe.

Dans la formule de T (n), θ (n²) omet en fait les coefficients constants avant n², car le symbole θ contient déjà tous les coefficients constants, mais le 8 dans la formule 8T (n / 2) ne peut pas être omis, car 8 représente un arbre récursif Chaque nœud a plusieurs nœuds enfants, ce qui détermine le nombre d'éléments que chaque couche de l'arbre contribue à la somme. Si 8 est omis, l'arbre récursif devient une structure linéaire.

L'idée centrale de l'algorithme de Strassen est de rendre l'arbre récursif un peu moins luxuriant, c'est-à-dire de n'effectuer que sept fois de manière récursive au lieu de huit multiplications matricielles n / 2Xn / 2. Le coût de la réduction d'une multiplication matricielle peut être un supplément nombre d'addition de matrice n / 2Xn / 2, mais uniquement des temps constants. L'algorithme comprend les étapes suivantes. Les étapes 2 à 4 expliqueront les étapes spécifiques plus tard:
1. Décomposez la matrice en fonction de l'indice. La méthode est la même que méthode récursive ordinaire, et cela prend du temps θ (1).
2. Créez 10 matrices n / 2Xn / 2, chaque matrice enregistre la somme ou la différence des deux sous-matrices créées à l'étape 1, et cela prend du temps θ (n²).
3. À l'aide de la sous-matrice créée à l'étape 1 et des 10 matrices créées à l'étape 2, calculez de manière récursive 7 produits matriciels, et le résultat de chaque produit matriciel est n / 2Xn / 2.
4. Calculez C11, C12, C21 et C22 de la matrice de résultat C en fonction du résultat du produit matriciel à l'étape 3.

Les étapes 1, 2 et 4 passent un total de θ (n²) temps, et l'étape 3 nécessite 7 fois n / 2Xn / 2 multiplications matricielles, de sorte que le temps d'exécution T (n) de l'algorithme de Strassen est obtenu:
Insérez la description de l'image ici
T (n) = θ (n) lg7 ), lg7 est compris entre 2,80 et 2,81.

À l'étape 2 de l'algorithme de Strassen, les 10 matrices suivantes sont créées: Les
Insérez la description de l'image ici
Insérez la description de l'image ici
étapes ci-dessus calculent 10 additions et soustractions de n / 2Xn / 2 matrices, ce qui prend θ (n²) temps.

À l'étape 3, calculez récursivement la multiplication de la matrice n / 2Xn / 2 7 fois: dans la
Insérez la description de l'image ici
formule ci - dessus, seule la multiplication dans la colonne du milieu doit être réellement calculée, et la colonne de droite illustre simplement la relation entre ces produits et le sous-matrice créée à l'étape 2.

Étape 4 Effectuez des opérations d'addition et de soustraction sur la matrice créée à l'étape 3:
Insérez la description de l'image ici
Développez le côté droit de la formule ci-dessus:
Insérez la description de l'image ici
et C12 est égal à:
Insérez la description de l'image ici
C21 est égal à:
Insérez la description de l'image ici
C22 est égal à:
Insérez la description de l'image ici
Méthode de substitution pour résoudre la formule récursive:
1. Devinez la forme de la solution.
2. Utilisez l'induction mathématique pour trouver les constantes dans la solution et prouver que la solution est correcte.

La méthode de substitution peut être utilisée pour trouver la borne supérieure de la formule récursive suivante:
Insérez la description de l'image ici
on suppose que la solution est O (nlgn), et la méthode de substitution nécessite la preuve que si la constante c> 0 est correctement sélectionnée, il peut y avoir T (n ) <= cnlgn. L'entier m <n est tout vrai, en particulier pour m = ⌊n / 2⌋, il y a T (⌊n / 2⌋) <= c⌊n / 2⌋lg (⌊n / 2⌋) , qui est substitué dans la formule récursive, Get:
Insérez la description de l'image ici
Parmi eux, tant que c> = 1, la dernière étape sera établie. La méthode d'induction mathématique nécessite la preuve que la solution est également valide dans les conditions aux limites. Supposons que T (1) = 1 est la seule condition aux limites (condition initiale) de la formule récursive. Pour n = 1, la condition aux limites T (n ) <= cnlgn dérive T (1) <= c1lg1 = 0, ce qui contredit T (1) = 1. A ce moment, n peut être étendu, en prenant n = 2 comme condition aux limites, et T (2) = 4 et T (3) = peut être calculé à partir de la formule récursive 5. A ce moment, la preuve inductive peut être complétée: pour une certaine constante c> = 1, T (n) <= cnlgn, la méthode consiste à choisir un assez grand c pour satisfaire T (2) <= c2lg2 et T (3) <= c3lg3.

Mais la méthode de substitution n'a pas de méthode universelle pour deviner la bonne solution récursive. Deviner la solution nécessite de l'expérience et parfois de la créativité. Vous pouvez emprunter un arbre récursif pour faire une bonne estimation. Si la formule récursive est similaire à la formule récursive que vous avez vue, il est raisonnable de deviner une solution similaire, telle que la formule récursive suivante: par
Insérez la description de l'image ici
rapport à l'exemple ci-dessus, il est seulement +17. Lorsque n est grand, il est proche à n la moitié, alors devinez T (n) = O (nlgn), cette supposition est correcte.

Une autre bonne méthode consiste à vérifier d'abord les limites supérieures et inférieures lâches de la formule récursive, puis à réduire la plage d'incertitude. Comme dans l'exemple ci-dessus, vous pouvez partir de la borne inférieure Ω (n), car la formule récursive contient le terme n. Nous pouvons prouver une borne supérieure O (n²), puis abaisser progressivement la borne supérieure et élever la borne inférieure jusqu'à ce qu'elle converge vers la borne asymptotiquement compacte θ (nlgn).

Parfois, la borne asymptotique est devinée, mais la preuve inductive échoue. À ce stade, modifiez la supposition et soustrayez-y un terme d'ordre inférieur. La preuve mathématique peut souvent se dérouler sans heurts, comme la formule récursive suivante:
Insérez la description de l'image ici
nous supposons que la solution est T (n) = O (N), et essayez de prouver que pour une constante propre c, T (n) <= cn est vraie, remplacez la supposition dans la formule récursive, et obtenez:
Insérez la description de l'image ici
Mais cela ne signifie pas que pour tout c, T (n) <cn, Nous pouvons deviner une borne plus grande, telle que T (n) = O (n²), bien que le résultat puisse être déduit, l'original T (n) = O (n) est correct.

Nous faisons une nouvelle supposition: T (n) <= cn-d, d est une constante ≥0, et maintenant il y a:
Insérez la description de l'image ici
tant que d≥1, cette formule tient, alors comme avant, choisissez un c assez grand pour traiter avec la condition aux limites.

Vous constaterez peut-être que l'idée de soustraire un terme d'ordre inférieur est contre-intuitive. Après tout, si la limite supérieure s'avère infructueuse, l'estimation doit être augmentée plutôt que diminuée, mais une limite plus lâche n'est pas nécessairement plus facile à prouver, parce que pour prouver un plus faible Pour la borne supérieure, la même borne plus faible doit être utilisée dans la preuve inductive.

Dans l'exemple ci-dessus, nous pouvons avoir la fausse preuve suivante:
Insérez la description de l'image ici
parce que c est une constante, l'erreur est que nous n'avons pas prouvé une forme strictement cohérente avec l'hypothèse d'induction, c'est-à-dire T (n) ≤cn.

Le reste est trop difficile à apprendre, peut-être plus tard

Je suppose que tu aimes

Origine blog.csdn.net/tus00000/article/details/114990966
conseillé
Classement