Dynamische Programmierung von Leetcode Hot 100 [rekursive Formel]

Inhaltsverzeichnis

Erste Schritte zum Verstehen

Fibonacci-Folge: Rekursion

Zahlenturm: Rekursion

Rekursionsformel

 Minimale Pfadsumme 

 Durchlaufreihenfolge

 Ganzzahlige Aufteilung: In Summe aufteilen, Produkt maximieren

Rucksack:: + -> Packen

rahmen

01 Rucksack: Kann nicht ausgewählt werden

In umgekehrter Reihenfolge verfahren

Wählen Sie i: Die untere rechte Ecke hängt von der oberen linken Ecke ab, um sicherzustellen, dass der Wert der vorherigen Ebene nicht überschrieben wird.

Wählen Sie i nicht aus: dp[i][v]=dp[i-1][v] Dieselbe Spalte ist nicht betroffen

In zwei Teile geteilt, Gewicht[i]==Wert[i]

Beispiel: dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])

Teilmengen gleicher Summe aufteilen: positive ganze Zahlen

Das Gewicht des letzten Steins II

Anzahl der Kombinationen: dp[j] += dp[j - nums[i]];

Zielsumme: nicht negative ganze Zahl, +/-

Kompletter Rucksack

sequentielle Durchquerung

Wählen Sie i: Die rechte Seite hängt von der linken Seite ab. Der Wert derselben Ebene wird verwendet. Die linke Seite muss zuerst generiert werden.

Wählen Sie i nicht aus: dp[i][v]=...dp[i-1][v] Dieselbe Spalte ist nicht betroffen

Anzahl der Optionen: dp[i] += dp[i - nums[j]]

Anzahl der Kombinationen: Außenartikel, Innenrucksack

Anzahl der Arrangements: Außenrucksack, Innenartikel

Wechselbörse II: Anzahl der Kombinationen

Kombinationssumme IV: Anzahl der Permutationen

Mindestanzahl

​​​​​​Änderungsaustausch

perfekte Quadratzahl

Beurteilung

Wortaufteilung

Teilmenge: dp[i] ist höchstens i optional

bester Wert

  Einsen und Nullen

In zwei Teile teilen: Gewicht[i]==Wert[i]

Folge: dp[i] endet mit dem Index i - 1/i (relative Reihenfolge beibehalten)

kontinuierlich

längster Palindrom-Teilstring

Maximale Teilsequenzsumme

längstes wiederholtes Subarray

nicht kontinuierlich

Längste gemeinsame Teilsequenz (LCS)

Disjunkte Linien: längste gemeinsame Teilsequenzlänge

verschiedene Teilsequenzen

längste aufsteigende Teilfolge

längste Palindrom-Folge

Raub: Teilmenge + bester Wert + kann nicht nacheinander gestohlen werden

Array

Ring

Binärbaum

Aktien kaufen und verkaufen

Einmal kaufen und verkaufen

Gierig: Nehmen Sie links den Mindestwert und rechts den Höchstwert. Die Differenz ist der maximale Gewinn.

Verkaufen Sie, bevor Sie erneut kaufen

Gierig: Gewinne gliedern sich in Tagesgeschäfte auf

Maximal zwei Transaktionen

Bis zu k Stifte

Gefrierzeit

Bearbeitungsgebühr


(Dynamische Programmierung, DP)

Rekursives oder rekursives Schreiben wird zur Implementierung dynamischer Programmierung verwendet. Das rekursive Schreiben wird hier auch als gespeicherte Suche bezeichnet.

Erste Schritte zum Verstehen


Fibonacci-Folge: Rekursion

function F(n){
if(n= 0||n== 1) return 1;
else return F(n-1)+F(n-2);
}

dp[n]=-1 bedeutet, dass F(n) noch nicht berechnet wurde

function F(n) {
if(n == 0||n==1) return 1;//递归边界
if(dp[n] != -1) return dp[n]; //已经计算过,直接返回结果,不再重复计算else {
else dp[n] = F(n-1) + F(n-2); //计算F(n),并保存至dp[n]
return dp [n];//返回F(n)的结果
}

Zahlenturm: Rekursion

Es gibt i-Zahlen in der i-ten Schicht. Jetzt müssen wir von der ersten Ebene zur n-ten Ebene gehen . Wie hoch ist die maximale Summe, die man erhält, wenn man alle Zahlen auf dem Pfad addiert?

dp[i][j] stellt die maximale Summe dar , die unter allen Pfaden von der j-ten Zahl in der i-ten Zeile bis zur untersten Ebene erhalten werden kann

dp[i][i]=max(dp[i-1][j],dp[i-1][j+1])+f[i][j]

Rekursionsformel

 Minimale Pfadsumme 

mxn-Matrix a, beginnend in der oberen linken Ecke, können Sie jedes Mal nur nach rechts oder nach unten gehen und schließlich die untere rechte Ecke erreichen. Die Summe aller Zahlen auf dem Pfad ist die Pfadsumme und die kleinste Pfadsumme unter allen Pfade werden ausgegeben.

dp[i][j] stellt den kürzesten Weg von i nach j dar

Die Zustandsübergangsgleichung beim Lösen von Teilproblemen : die rekursive Formel vom „vorherigen Zustand“ zum „nächsten Zustand“.

dp[i, j] = min(dp[i - 1][j], dp[i][j - 1]) + Matrix[i][j]

In JavaScript gibt es kein Konzept für zweidimensionale Arrays, aber Sie können den Wert eines Array-Elements so festlegen, dass er dem Array entspricht

Schlüssel:

  1. dp[0][i] = dp[0][i - 1] + Matrix[0][i];
  2. dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + Matrix[i][j];
function minPathSum(matrix) {
    var row = matrix.length,
        col = matrix[0].length;
    var dp = new Array(row).fill(null).map(() => new Array(col).fill(0));
    dp[0][0] = matrix[0][0]; // 初始化左上角元素
    // 初始化第一行
    for (var i = 1; i < col; i++) dp[0][i] = dp[0][i - 1] + matrix[0][i];
    // 初始化第一列
    for (var j = 1; j < row; j++) dp[j][0] = dp[j - 1][0] + matrix[j][0];
    // 动态规划
    for (var i = 1; i < row; i++) {
        for (var j = 1; j < col; j++) {
            dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
        }
    }
    return dp[row - 1][col - 1]; // 右下角元素结果即为答案
}

 Durchlaufreihenfolge

 Ganzzahlige Aufteilung: In Summe aufteilen, Produkt maximieren

Tatsächlich können Sie j von 1 aus durchlaufen und dann dp [i] auf zwei Arten erhalten.

In 2 Zahlen aufteilen: j * (i - j) werden direkt multipliziert.

In 3 oder mehr Zahlen aufteilen: j * dp[i - j], was der Aufteilung von (i - j) entspricht.

dp[i] hängt vom Zustand von dp[i - j] ab, daher muss das Durchqueren von i von vorne nach hinten durchlaufen werden, zuerst gibt es dp[i - j] und dann gibt es dp[i]

var integerBreak = function(n) {
    let dp = new Array(n + 1).fill(0)
    dp[2] = 1

    for(let i = 3; i <= n; i++) {
        for(let j = 1; j <= i / 2; j++) {
            dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j)
        }
    }
    return dp[n]
};

Rucksack:: + -> Packen

rahmen

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            把物品 i 装进背包,
            不把物品 i 装进背包
        )
return dp[N][W]

...加上边界条件,装不下的时候,只能选择不装

01 Rucksack: Kann nicht ausgewählt werden

Es gibt n Elemente mit Gewicht w[i], Wert c[j] und Kapazität V. Von jedem Element gibt es nur ein Element.
dp[i][v] stellt den Maximalwert dar, der durch Laden der ersten i Elemente (1≤i≤n, 0≤v≤V) in einen Rucksack mit der Kapazität v erhalten werden kann.


function testWeightBagProblem (weight, value, size) {
    // 定义 dp 数组
    const len = weight.length,
          dp = Array(len).fill().map(() => Array(size + 1).fill(0));

    // 初始化
    for(let j = weight[0]; j <= size; j++) {
        dp[0][j] = value[0];
    }

    // weight 数组的长度len 就是物品个数
    for(let i = 1; i < len; i++) { // 遍历物品
        for(let j = 0; j <= size; j++) { // 遍历背包容量
            if(j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }

    console.table(dp)

    return dp[len - 1][size];
}

function test () {
    console.log(testWeightBagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6));
}

test();

In umgekehrter Reihenfolge verfahren

Wählen Sie i: Die untere rechte Ecke hängt von der oberen linken Ecke ab, um sicherzustellen, dass der Wert der vorherigen Ebene nicht überschrieben wird.

Wählen Sie i nicht aus: dp[i][v]=dp[i-1][v] Dieselbe Spalte ist nicht betroffen

Es handelt sich im Wesentlichen um eine Durchquerung eines zweidimensionalen Arrays. Der Wert in der unteren rechten Ecke hängt vom Wert in der oberen linken Ecke der vorherigen Ebene ab. Daher muss sichergestellt werden, dass der Wert auf der linken Seite immer noch von der vorherigen Ebene abhängt vorherige Schicht und deckt sie von rechts nach links ab.

for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

In zwei Teile geteilt, Gewicht[i]==Wert[i]

Wenn Sie ein eindimensionales dp-Array verwenden, wird die for-Schleife, die die Elemente (Wert) durchläuft, in der äußeren Schicht platziert, die for-Schleife, die den Rucksack (Gewicht) durchläuft , wird in der inneren Schicht platziert und die innere for-Schleife wird in der äußeren Schicht platziert in umgekehrter Reihenfolge durchlaufen ! scrollendes Array

Beispiel: dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])

Teilmengen gleicher Summe aufteilen: positive ganze Zahlen
  • Das Volumen des Rucksacks beträgt Summe / 2
  • Das Gewicht des Produkts (das Element in der Kollektion), das in den Rucksack gelegt werden soll, ist der Wert des Elements, und der Wert ist auch der Wert des Elements.
  • Wenn der Rucksack genau voll ist, bedeutet dies, dass eine Teilmenge mit einer Summe von Summe / 2 gefunden wurde.
  • Jedes Element im Rucksack kann nicht wiederholt platziert werden.
var canPartition = function(nums) {
    const sum = (nums.reduce((p, v) => p + v));
//奇数
    if (sum & 1) return false;
    const dp = Array(sum / 2 + 1).fill(0);
    for(let i = 0; i < nums.length; i++) {
        for(let j = sum / 2; j >= nums[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            if (dp[j] === sum / 2) {
                return true;
            }
        }
    }
    return dp[sum / 2] === sum / 2;
};
Das Gewicht des letzten Steins II
/**
 * @param {number[]} stones
 * @return {number}
 */
var lastStoneWeightII = function (stones) {
    let sum = stones.reduce((s, n) => s + n);

    let dpLen = Math.floor(sum / 2);
    let dp = new Array(dpLen + 1).fill(0);

    for (let i = 0; i < stones.length; ++i) {
        for (let j = dpLen; j >= stones[i]; --j) {
            dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
        }
    }

    return sum - dp[dpLen] - dp[dpLen];
};

Anzahl der Kombinationen: dp[j] += dp[j - nums[i]];

Zielsumme: nicht negative ganze Zahl, +/-

linke Kombination - rechte Kombination = Ziel.

links + rechts = Summe, und die Summe ist fest. rechts = Summe - links

Hier kommt die Formel: links – (Summe – links) = Ziel leitet links = (Ziel + Summe)/2 ab.

Das Ziel ist festgelegt, die Summe ist festgelegt und der Rest kann gefunden werden.

Das Problem besteht zu diesem Zeitpunkt darin, die Kombination zu finden, deren Summe in den eingestellten Zahlen übrig bleibt

dp[j] bedeutet: Es gibt dp[j]-Möglichkeiten, einen Beutel mit einem Volumen von j (einschließlich j) zu füllen.

Solange Sie nums[i] erhalten, gibt es dp[j – nums[i]]-Möglichkeiten, dp[j] zu erstellen.

Zum Beispiel: dp[j], j ist 5,

  • Wenn bereits eine 1 (nums[i]) vorhanden ist, gibt es dp[4]-Möglichkeiten, einen Rucksack mit einer Kapazität von 5 herzustellen.
  • Wenn Sie bereits eine 2(nums[i]) haben, gibt es dp[3]-Möglichkeiten, einen Rucksack mit einer Kapazität von 5 herzustellen.
  • Wenn Sie bereits eine 3 (nums[i]) haben, gibt es in dp[2] eine Methode, um einen Rucksack mit einer Kapazität von 5 zu erstellen
  • Wenn Sie bereits eine 4 (nums[i]) haben, gibt es in dp[1] eine Methode, um einen Rucksack mit einer Kapazität von 5 zu erstellen
  • Wenn Sie bereits eine 5 (nums[i]) haben, gibt es in dp[0] eine Methode, um einen Rucksack mit einer Kapazität von 5 zu erstellen

Wie viele Möglichkeiten gibt es also, dp[5] aufzurunden, also alle dp[j - nums[i]] zu addieren?

Daher ähneln die Formeln zur Lösung von Kombinationsproblemen dieser:

dp[j] += dp[j - nums[i]]
const findTargetSumWays = (nums, target) => {

    const sum = nums.reduce((a, b) => a+b);
    
    if(Math.abs(target) > sum) {
        return 0;
    }

    if((target + sum) % 2) {
        return 0;
    }

    const halfSum = (target + sum) / 2;

    let dp = new Array(halfSum+1).fill(0);
    dp[0] = 1;

    for(let i = 0; i < nums.length; i++) {
        for(let j = halfSum; j >= nums[i]; j--) {
            dp[j] += dp[j - nums[i]];
        }
    }

    return dp[halfSum];
};

Kompletter Rucksack

Der Unterschied zum 01-Rucksackproblem besteht darin, dass es von jedem Gegenstand unzählige Teile gibt.

sequentielle Durchquerung

Wählen Sie i: Die rechte Seite hängt von der linken Seite ab. Der Wert derselben Ebene wird verwendet. Die linke Seite muss zuerst generiert werden.

Wählen Sie i nicht aus: dp[i][v]=...dp[i-1][v] Dieselbe Spalte ist nicht betroffen

// 先遍历物品,再遍历背包容量
function test_completePack1() {
    let weight = [1, 3, 5]
    let value = [15, 20, 30]
    let bagWeight = 4 
    let dp = new Array(bagWeight + 1).fill(0)
    for(let i = 0; i <= weight.length; i++) {
        for(let j = weight[i]; j <= bagWeight; j++) {
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
        }
    }
    console.log(dp)
}

// 先遍历背包容量,再遍历物品
function test_completePack2() {
    let weight = [1, 3, 5]
    let value = [15, 20, 30]
    let bagWeight = 4 
    let dp = new Array(bagWeight + 1).fill(0)
    for(let j = 0; j <= bagWeight; j++) {
        for(let i = 0; i < weight.length; i++) {
            if (j >= weight[i]) {
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
            }
        }
    }
    console.log(2, dp);
}

Die Reihenfolge der beiden Schlaufen des kompletten Rucksacks ist akzeptabel.

Anzahl der Optionen: dp[i] += dp[i - nums[j]]

Anzahl der Kombinationen: Außenartikel, Innenrucksack

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

Angenommen: Münzen[0] = 1, Münzen[1] = 5.

Addieren Sie zuerst 1 zur Berechnung und dann 5 zur Berechnung. Die Anzahl der erhaltenen Methoden beträgt nur {1, 5}. Und die Situation von {5, 1} wird nicht eintreten

Anzahl der Arrangements: Außenrucksack, Innenartikel

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

Jeder Wert der Rucksackkapazität wird aus 1 und 5 berechnet, einschließlich {1, 5} und {5, 1}.

dp[j] befindet sich in der äußeren Schicht. Wenn 1 angetroffen wird, wird 5 aktualisiert. Wenn 5 angetroffen wird, wird 1 aktualisiert.

Wechselbörse II: Anzahl der Kombinationen

Gegeben sind Münzen verschiedener Nennwerte und ein Gesamtbetrag. Schreiben Sie eine Funktion, um die Anzahl der Münzkombinationen zu berechnen, aus denen sich der Gesamtbetrag zusammensetzt. Angenommen, es gibt unendlich viele Münzen jedes Nennwerts.

Beispiel 1:

  • Eingabe: Betrag = 5, Münzen = [1, 2, 5]
  • Ausgabe: 4

Erläuterung: Es gibt vier Möglichkeiten, den Gesamtbetrag aufzurunden:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1
const change = (amount, coins) => {
    let dp = Array(amount + 1).fill(0);
    dp[0] = 1;

    for(let i =0; i < coins.length; i++) {
        for(let j = coins[i]; j <= amount; j++) {
            dp[j] += dp[j - coins[i]];
        }
    }

    return dp[amount];
}

Kombinationssumme IV: Anzahl der Permutationen

Stufen steigen

const combinationSum4 = (nums, target) => {

    let dp = Array(target + 1).fill(0);
    dp[0] = 1;

    for(let i = 0; i <= target; i++) {
        for(let j = 0; j < nums.length; j++) {
            if (i >= nums[j]) {
                dp[i] += dp[i - nums[j]];
            }
        }
    }

    return dp[target];
};

Mindestanzahl

​​​​​​Änderungsaustausch

Der reine vollständige Rucksack bestimmt den Maximalwert eines vollen Rucksacks. Es spielt keine Rolle, ob die Elemente, aus denen sich die Summe zusammensetzt, in Ordnung sind. Das heißt, es spielt keine Rolle, ob sie in Ordnung sind oder nicht!

// 遍历物品
const coinChange = (coins, amount) => {
    if(!amount) {
        return 0;
    }

    let dp = Array(amount + 1).fill(Infinity);
    dp[0] = 0;

    for(let i = 0; i < coins.length; i++) {
        for(let j = coins[i]; j <= amount; j++) {
            dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
        }
    }

    return dp[amount] === Infinity ? -1 : dp[amount];
}

perfekte Quadratzahl

Eine perfekte Quadratzahl ist ein Gegenstand (kann in unbegrenzten Stücken verwendet werden) und eine positive ganze Zahl n ist ein Rucksack. Wie viele Gegenstände gibt es mindestens, um diesen Rucksack zu vervollständigen?

for (int j = 1; j * j <= i; j++) { // 遍历物品
        dp[i] = min(dp[i - j * j] + 1, dp[i]);
    }

Beurteilung

Wortaufteilung

Wörter sind Elemente, und String s ist der Rucksack. Ob Wörter String s bilden können, ist die Frage, ob die Elemente den Rucksack füllen können.

Bei der Aufteilung können Sie Wörter im Wörterbuch wiederverwenden, was bedeutet, dass Sie einen kompletten Rucksack haben!

dp[i]: Wenn die Zeichenfolgenlänge i ist, ist dp[i] wahr, was bedeutet, dass es in ein oder mehrere Wörter aufgeteilt werden kann, die im Wörterbuch erscheinen .

Wenn festgestellt wird, dass dp[j] wahr ist und die Teilzeichenfolge des Intervalls [j, i] im Wörterbuch erscheint, muss dp[i] wahr sein. (j < i ).

Die Rekursionsformel lautet also: if([j, i] die Teilzeichenfolge dieses Intervalls erscheint im Wörterbuch && dp[j] ist wahr), dann ist dp[i] = wahr.

const wordBreak = (s, wordDict) => {

    let dp = Array(s.length + 1).fill(false);
    dp[0] = true;

    for(let i = 0; i <= s.length; i++){
        for(let j = 0; j < wordDict.length; j++) {
            if(i >= wordDict[j].length) {
                if(s.slice(i - wordDict[j].length, i) === wordDict[j] && dp[i - wordDict[j].length]) {
                    dp[i] = true
                }
            }
        }
    }

    return dp[s.length];
}

Teilmenge: dp[i] ist höchstens i optional

bester Wert

  Einsen und Nullen

Finden Sie die Größe der größten Teilmenge von Strs, die höchstens m 0s und n 1s hat, und geben Sie sie zurück.

  • Beispiel: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3

  • Ausgabe: 4

  • Erläuterung: Die größte Teilmenge mit höchstens 5 Nullen und 3 Einsen ist {"10", "0001", "1", "0"}, daher lautet die Antwort 4. Andere kleinere Teilmengen, die die Frage erfüllen, sind {"0001", "1"} und {"10", "1", "0"}. {"111001"} erfüllt die Bedeutung der Frage nicht, da es 4 Einsen enthält, was größer als der Wert von n ist

dp[i][j]: Die Größe der größten Teilmenge von Strs mit höchstens i 0s und j 1s ist dp[i][j] .

Bestimmen Sie die Wiederholungsformel

dp[i][j] kann aus der Zeichenfolge in den vorherigen Strs abgeleitet werden. Die Zeichenfolge in Strs hat nullNum 0s und oneNum 1s.

dp[i][j] kann dp[i - zeroNum][j - oneNum] + 1 sein.

Dann nehmen wir während des Durchquerungsprozesses den Maximalwert von dp[i][j] an.

Also die Rekursionsformel: dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

An dieser Stelle können Sie sich an die Rekursionsformel des 01-Rucksacks erinnern: dp[j] = max(dp[j], dp[j - Gewicht[i]] + Wert[i]);

Wenn Sie es vergleichen, werden Sie feststellen, dass die Zeichenfolgen „zeroNum“ und „oneNum“ dem Gewicht des Artikels entsprechen (Gewicht [i]).

const findMaxForm = (strs, m, n) => {
    const dp = Array.from(Array(m+1), () => Array(n+1).fill(0));
    let numOfZeros, numOfOnes;

    for(let str of strs) {
        numOfZeros = 0;
        numOfOnes = 0;
    
        for(let c of str) {
            if (c === '0') {
                numOfZeros++;
            } else {
                numOfOnes++;
            }
        }

        for(let i = m; i >= numOfZeros; i--) {
            for(let j = n; j >= numOfOnes; j--) {
                dp[i][j] = Math.max(dp[i][j], dp[i - numOfZeros][j - numOfOnes] + 1);
            }
        }
    }

    return dp[m][n];
};

In zwei Teile teilen: Gewicht[i]==Wert[i]

Folge: dp[i] endet mit dem Index i - 1/i (relative Reihenfolge beibehalten)

 for(let i = 1; i < nums.length; i++) 
        for(let j = 0; j < i; j++) 

kontinuierlich

längster Palindrom-Teilstring

dp[i][j] gibt an, ob der durch S[i] bis S[j] dargestellte Teilstring ein Palindrom-Teilstring ist. Wenn ja, ist er 1, wenn nicht, ist er 0.

Maximale Teilsequenzsumme

dp[i]: Die maximale fortlaufende Teilsequenzsumme einschließlich Index i (endet mit nums[i]) ist dp[i] .

dp[i] = max(dp[i - 1] + nums[i], nums[i]);

längstes wiederholtes Subarray

dp[i][j]: A endet mit dem Index i – 1 und B endet mit dem Index j – 1. Die Länge des längsten wiederholten Subarrays beträgt dp[i][j].

Wenn A[i - 1] und B[j - 1] gleich sind, gilt dp[i][j] = dp[i - 1][j - 1] + 1;

dynamische Programmierung

const findLength = (A, B) => {
    // A、B数组的长度
    const [m, n] = [A.length, B.length];
    // dp数组初始化,都初始化为0
    const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));
    // 初始化最大长度为0
    let res = 0;
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            // 遇到A[i - 1] === B[j - 1],则更新dp数组
            if (A[i - 1] === B[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }
            // 更新res
            res = dp[i][j] > res ? dp[i][j] : res;
        }
    }
    // 遍历完成,返回res
    return res;
};

scrollendes Array

const findLength = (nums1, nums2) => {
    let len1 = nums1.length, len2 = nums2.length;
    // dp[i][j]: 以nums1[i-1]、nums2[j-1]为结尾的最长公共子数组的长度
    let dp = new Array(len2+1).fill(0);
    let res = 0;
    for (let i = 1; i <= len1; i++) {
        for (let j = len2; j > 0; j--) {
            if (nums1[i-1] === nums2[j-1]) {
                dp[j] = dp[j-1] + 1;
            } else {
                dp[j] = 0;
            }
            res = Math.max(res, dp[j]);
        }
    }
    return res;
}

nicht kontinuierlich

Längste gemeinsame Teilsequenz (LCS)

Längste gemeinsame Teilsequenz: Die Teilsequenz kann diskontinuierlich
„sadstory“ und „adminsorry“ sein. Die längste gemeinsame Teilsequenz ist „adsory“

dp[i][j]: LCS-Länge vor strA[i] und strB[j] , der Index beginnt bei 1

Disjunkte Linien: längste gemeinsame Teilsequenzlänge

Gerade Linien können sich nicht schneiden, was bedeutet, dass in Zeichenfolge A eine Teilfolge gefunden wird, die mit Zeichenfolge B identisch ist, und diese Teilfolge die relative Reihenfolge nicht ändern kann. Solange sich die relative Reihenfolge nicht ändert, schneiden sich gerade Linien, die dieselben Zahlen verbinden, nicht.

verschiedene Teilsequenzen

dp[i][j]: Die Anzahl der ts, die mit j-1 enden und in der s-Teilsequenz erscheinen, die mit i-1 endet, ist dp[i][j]

const numDistinct = (s, t) => {
    let dp = Array.from(Array(s.length + 1), () => Array(t.length +1).fill(0));

    for(let i = 0; i <=s.length; i++) {
        dp[i][0] = 1;
    }
    
    for(let i = 1; i <= s.length; i++) {
        for(let j = 1; j<= t.length; j++) {
            if(s[i-1] === t[j-1]) {
//不用s[i - 1]来匹配,个数为dp[i - 1][j]
                dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
            } else {
                dp[i][j] = dp[i-1][j]
            }
        }
    }

    return dp[s.length][t.length];
};

längste aufsteigende Teilfolge

dp[i] stellt die Länge der am längsten ansteigenden Teilsequenz dar, die mit nums[i] endet und i vor i enthält.

Die längste aufsteigende Teilfolge der Position i ist gleich dem Maximalwert der längsten aufsteigenden Teilfolge + 1 jeder Position von j von 0 bis i-1.

Beispiel: if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

const lengthOfLIS = (nums) => {
    let dp = Array(nums.length).fill(1);
    let result = 1;

    for(let i = 1; i < nums.length; i++) {
        for(let j = 0; j < i; j++) {
            if(nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j]+1);
            }
        }
        result = Math.max(result, dp[i]);
    }

    return result;
};

längste Palindrom-Folge

const longestPalindromeSubseq = (s) => {
    const strLen = s.length;
    let dp = Array.from(Array(strLen), () => Array(strLen).fill(0));

    for(let i = 0; i < strLen; i++) {
        dp[i][i] = 1;
    }

    for(let i = strLen - 1; i >= 0; i--) {
        for(let j = i + 1; j < strLen; j++) {
            if(s[i] === s[j]) {
                dp[i][j] = dp[i+1][j-1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
            }
        }
    }

    return dp[0][strLen - 1];
};

Raub: Teilmenge + bester Wert + kann nicht nacheinander gestohlen werden

Array

dp[i]: Unter Berücksichtigung der Häuser innerhalb des Index i (einschließlich i) beträgt der maximale Betrag, der gestohlen werden kann, dp[i] .

Der Faktor, der dp[i] bestimmt, ist, ob der i-te Raum gestohlen wird oder nicht.

Wenn der i-te Raum gestohlen wird, dann ist dp[i] = dp[i - 2] + nums[i], das heißt: der i-1-te Raum darf nicht berücksichtigt werden, suchen Sie den Index i-2 (einschließlich i- 2) Für Häuser darin beträgt der maximale Betrag, der gestohlen werden kann, dp[i-2] plus das aus dem i-ten Raum gestohlene Geld.

Wenn Sie den i-ten Raum nicht stehlen, ist dp[i] = dp[i - 1], das heißt, berücksichtigen Sie den i-1-Raum. ( Beachten Sie, dass dies hier berücksichtigt wird und nicht unbedingt bedeutet, dass Sie muss den i-1-Raum stehlen. Dies wird von vielen Schülern leicht verwechselt. Punkt )

Dann nimmt dp[i] den Maximalwert an, dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

Ring

Betroffen sind das erste und das letzte Element

Für ein Array gibt es drei Hauptsituationen, in denen es einen Ring bildet:

  • Fall 1: Erwägen Sie, das erste und letzte Element nicht zu enthalten = lineares Array

213. Uchika Kasha II

  • Fall 2: Erwägen Sie die Einbeziehung des ersten Elements, jedoch nicht des letzten Elements

213. Raub II1

  • Szenario 3: Erwägen Sie die Einbeziehung des Schwanzelements, jedoch nicht des ersten Elements

213. Raub II2

Beachten Sie, dass ich hier „berücksichtigen“ verwende . In Fall 3 gilt beispielsweise, dass das Schwanzelement zwar als enthalten betrachtet wird, das Schwanzelement jedoch nicht ausgewählt werden muss! Im dritten Fall ist die Verwendung von nums[1] und nums[3] am größten.

Situation 2 und Situation 3 umfassen beide Situation 1, sodass nur Situation 2 und Situation 3 berücksichtigt werden können .

var rob = function(nums) {
  const n = nums.length
  if (n === 0) return 0
  if (n === 1) return nums[0]
  const result1 = robRange(nums, 0, n - 2)
  const result2 = robRange(nums, 1, n - 1)
  return Math.max(result1, result2)
};

const robRange = (nums, start, end) => {
  if (end === start) return nums[start]
  const dp = Array(nums.length).fill(0)
  dp[start] = nums[start]
  dp[start + 1] = Math.max(nums[start], nums[start + 1])
  for (let i = start + 2; i <= end; i++) {
    dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1])
  }
  return dp[end]
}

Binärbaum

Der Index 0 zeichnet den maximalen Geldbetrag auf, der durch den Nichtdiebstahl des Knotens erzielt wird, und der Index 1 zeichnet den maximalen Geldbetrag auf, der durch den Diebstahl des Knotens erzielt wird.

Das dp-Array in dieser Frage ist also ein Array mit einer Länge von 2!

Dann fragen sich einige Schüler vielleicht: Wie kann ein Array der Länge 2 den Status jedes Knotens im Baum markieren?

Vergessen Sie nicht, dass der Systemstapel während des rekursiven Prozesses die Parameter jeder Rekursionsebene speichert .

Durchquerung nach der Bestellung: Verwenden Sie den Rückgabewert der rekursiven Funktion, um die nächste Berechnung durchzuführen

const rob = root => {
    // 后序遍历函数
    const postOrder = node => {
        // 递归出口
        if (!node) return [0, 0];
        // 遍历左子树
        const left = postOrder(node.left);
        // 遍历右子树
        const right = postOrder(node.right);
        // 不偷当前节点,左右子节点都可以偷或不偷,取最大值
        const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        // 偷当前节点,左右子节点只能不偷
        const Do = node.val + left[0] + right[0];
        // [不偷,偷]
        return [DoNot, Do];
    };
    const res = postOrder(root);
    // 返回最大值
    return Math.max(...res);
};

Aktien kaufen und verkaufen

Einmal kaufen und verkaufen

Zuerst wird das Bargeld auf 0 gesetzt. Der Kauf von Aktien kostet Geld (und wird zu einer negativen Zahl), und der Verkauf von Aktien bringt Geld.
Jeden Tag haben Aktien zwei Zustände: gehalten und nicht gehalten.
Für die rekursive Formel dp[i] [0] = max(dp[i - 1][0], -price[i]),
tatsächlich bedeutet es, ob die Aktie am Tag i gekauft werden soll. Wenn Sie sie kaufen, wird das Geld reduziert. Wenn Sie Kaufen Sie es nicht, der Status vom Vortag bleibt erhalten. Nehmen Sie dasjenige mit dem meisten verbleibenden Geld zwischen den beiden Situationen Kaufen oder Nichtkaufen.
Für dp[i][1] = max(dp[i - 1 ][1], dp[i - 1][0] + Preis[i])
Es bedeutet, ob die Aktie am Tag i verkauft werden soll. Wenn Sie sie verkaufen, werden Sie Geld verdienen. Wenn Sie sie nicht verkaufen, Sie behält den Status vom Vortag bei. Derjenige mit dem meisten verbleibenden Geld wird zwischen Verkauf und Nichtverkauf genommen. Wenn Sie die Aktie verkaufen möchten, müssen Sie die Aktie zuvor gekauft haben. Es ist also dp[i - 1] [0] + Preis[i]

Gierig: Nehmen Sie links den Mindestwert und rechts den Höchstwert. Die Differenz ist der maximale Gewinn.

int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]);  // 取最左最小价格
            result = max(result, prices[i] - low); // 直接取最大区间利润
        }
        return result;
    }

Verkaufen Sie, bevor Sie erneut kaufen

const maxProfit = (prices) => {
  let dp = Array.from(Array(prices.length), () => Array(2).fill(0));
  // dp[i][0] 表示第i天持有股票所得现金。
  // dp[i][1] 表示第i天不持有股票所得最多现金
  dp[0][0] = 0 - prices[0];
  dp[0][1] = 0;
  for (let i = 1; i < prices.length; i++) {
    // 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
    // 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
    // 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
    dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);

    // 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
    // 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
    // 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
    dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
  }

  return dp[prices.length - 1][1];
};

Gierig: Gewinne gliedern sich in Tagesgeschäfte auf

Wenn Sie am Tag 0 kaufen und am Tag 3 verkaufen, beträgt der Gewinn: Preise[3] – Preise[0].

相当于(Preise[3] - Preise[2]) + (Preise[2] - Preise[1]) + (Preise[1] - Preise[0])。

Zu diesem Zeitpunkt wird der Gewinn in tägliche Einheitsdimensionen zerlegt, anstatt ihn als Ganzes von Tag 0 bis Tag 3 zu betrachten.

var maxProfit = function(prices) {
    let result = 0
    for(let i = 1; i < prices.length; i++) {
        result += Math.max(prices[i] - prices[i - 1], 0)
    }
    return result
};

Maximal zwei Transaktionen

  1. Bestimmen Sie die Bedeutung des dp-Arrays und der Indizes

An einem Tag gibt es insgesamt fünf Zustände,

  1. Keine Operation (tatsächlich müssen wir diesen Status nicht festlegen)
  2. Zum ersten Mal Aktien halten
  3. Zum ersten Mal keine Aktien halten
  4. Zum zweiten Mal Aktien halten
  5. Zum zweiten Mal keine Aktien halten

i in dp[i][j] stellt den i-ten Tag dar, j ist die fünf Staaten [0 - 4], dp[i][j] stellt das maximale Bargeld dar, das am i-ten Tag im Staat j verbleibt

  1. Bestimmen Sie die Wiederholungsformel

Um den Zustand dp[i][1] zu erreichen, gibt es zwei spezifische Vorgänge:

  • Operation 1: Wenn Sie die Aktie am i-ten Tag kaufen, dann gilt dp[i][1] = dp[i-1][0] – Preise[i]
  • Operation 2: Am i-ten Tag findet keine Operation statt, es wird jedoch der Kaufstatus des Vortages verwendet, das heißt: dp[i][1] = dp[i - 1][1]

Sollte dp[i][1] also dp[i-1][0] - Preise[i] oder dp[i - 1][1] wählen?

Der größte muss gewählt werden, also dp[i][1] = max(dp[i-1][0] - Preise[i], dp[i - 1][1]);

Auf die gleiche Weise verfügt dp[i][2] auch über zwei Operationen:

  • Operation 1: Wenn die Aktie am Tag i verkauft wird, dann gilt dp[i][2] = dp[i - 1][1] + Preise[i]
  • Vorgang 2: Am i-ten Tag findet kein Vorgang statt und es wird der Verkaufsstatus des Bestands am Vortag verwendet, d. h.: dp[i][2] = dp[i - 1][2]

所以dp[i][2] = max(dp[i - 1][1] + Preise[i], dp[i - 1][2])

Auf die gleiche Weise lässt sich der verbleibende Statusteil ableiten:

dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - Preise[i]);

dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + Preise[i]);

  1. So initialisieren Sie das DP-Array

Am Tag 0 findet keine Operation statt. Am einfachsten kann man sich 0 vorstellen, das heißt: dp[0][0] = 0;

Führen Sie den ersten Kaufvorgang am Tag 0 durch, dp[0][1] = -prices[0];

Wie hoch sollte der Anfangswert sein, wenn der erste Verkaufsvorgang am Tag 0 durchgeführt wird?

Wenn Sie es noch nicht gekauft haben, wie können Sie es verkaufen? Tatsächlich kann jeder verstehen, dass der Kauf am selben Tag und der Verkauf am selben Tag erfolgen, also dp[0][2] = 0;

Wie hoch sollte der Anfangswert für den zweiten Kaufvorgang am Tag 0 sein? Viele Studenten fragen sich vielleicht: Wie kann man den zweiten Kauf initialisieren, wenn man beim ersten Mal noch nicht gekauft hat?

Der zweite Kauf hängt vom Status des ersten Verkaufs ab. Tatsächlich entspricht er dem ersten Kauf am Tag 0, dem ersten Verkauf und dem erneuten Kauf (zweiter Kauf) und dann jetzt Wenn Sie dies nicht tun Bargeld zur Hand haben, solange Sie kaufen, wird Ihr Bargeld entsprechend reduziert.

Daher wird der zweite Kaufvorgang wie folgt initialisiert: dp[0][3] = -prices[0];

Initialisieren Sie auf die gleiche Weise dp[0][4] = 0 für den zweiten Verkauf;

Bis zu k Stifte

  • 0 bedeutet keine Operation
  • 1 Erstkauf
  • 2 Erster Verkauf
  • 3 Sekunden Kauf
  • 4 Zweiter Verkauf
  • .....

Mit Ausnahme von 0 verkaufen gerade Zahlen und kaufen ungerade Zahlen .

Die Frage erfordert, dass es höchstens K Transaktionen gibt, dann ist der Bereich von j als 2 * k + 1 definiert.

// 方法一:动态规划
const maxProfit = (k,prices) => {
    if (prices == null || prices.length < 2 || k == 0) {
        return 0;
    }
    
    let dp = Array.from(Array(prices.length), () => Array(2*k+1).fill(0));

    for (let j = 1; j < 2 * k; j += 2) {
        dp[0][j] = 0 - prices[0];
    }
    
    for(let i = 1; i < prices.length; i++) {
        for (let j = 0; j < 2 * k; j += 2) {
            dp[i][j+1] = Math.max(dp[i-1][j+1], dp[i-1][j] - prices[i]);
            dp[i][j+2] = Math.max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]);
        }
    }

    return dp[prices.length - 1][2 * k];
};

// 方法二:动态规划+空间优化
var maxProfit = function(k, prices) {
    let n = prices.length;
    let dp = new Array(2*k+1).fill(0);
    // dp 买入状态初始化
    for (let i = 1; i <= 2*k; i += 2) {
        dp[i] = - prices[0];
    }

    for (let i = 1; i < n; i++) {
        for (let j = 1; j < 2*k+1; j++) {
            // j 为奇数:买入状态
            if (j % 2) {
                dp[j] = Math.max(dp[j], dp[j-1] - prices[i]);
            } else {
                // j为偶数:卖出状态
                dp[j] = Math.max(dp[j], dp[j-1] + prices[i]);
            }
        }
    }

    return dp[2*k];
};

Gefrierzeit

Vier Staaten:

  • Status 1: Status „Aktien halten“ (Aktien heute kaufen oder Aktien vorher kaufen und sie dann ohne Operation halten)
  • Wenn Sie nicht über den Lagerbestandsstatus verfügen, gibt es zwei Verkaufsbestandsstatus.
    • Status 2: Behalten Sie den Status des Verkaufs von Lagerbeständen bei (die Bestände wurden vor zwei Tagen verkauft und waren für einen Tag eingefroren. Oder die Bestände wurden am Vortag verkauft und es hat keine Operation stattgefunden)
    • Status 3: Aktien heute verkaufen
  • Status 4: Heute ist der Frostperiodenzustand, aber der Frostperiodenzustand ist nicht nachhaltig und dauert nur einen Tag!

Beim Erreichen des Aktienkaufstatus (Status 1), d. h. dp[i][0], gibt es zwei spezifische Vorgänge:

  • Vorgang 1: Die Aktie wurde am Vortag gehalten (Zustand 1), dp[i][0] = dp[i - 1][0]
  • Vorgang 2: Ich habe es heute gekauft, es gibt zwei Situationen
    • Der Vortag war die Gefrierperiode (Zustand vier), dp[i - 1][3] - Preise[i]
    • Der Vortag befand sich im Zustand des Verkaufs von Lagerbeständen (Zustand 2), dp[i - 1][1] - Preise[i]

那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - Preise[i], dp[i - 1][1] - Preise[i] );

Um den Lagerverkaufsstatus (Status 2) aufrechtzuerhalten, d. h. dp[i][1], gibt es zwei spezifische Vorgänge:

  • Vorgang 1: Der Vortag befand sich im Zustand 2
  • Vorgang 2: Der Vortag ist die Gefrierperiode (Zustand 4)

dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

Wenn heute der Stand des Aktienverkaufs erreicht wird (Zustand drei), d. h. dp[i][2], gibt es nur eine Operation:

Die Aktie muss gestern gehalten worden sein (Zustand 1) und heute verkauft worden sein

即:dp[i][2] = dp[i - 1][0] + Preise[i];

Beim Erreichen des Gefrierperiodenzustands (Zustand vier), d. h. dp[i][3], gibt es nur einen Vorgang:

Gestern Lagerbestände verkauft (Status drei)

dp[i][3] = dp[i - 1][2];

Um die obige Analyse zusammenzufassen, lautet der rekursive Code wie folgt:

dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];

Bearbeitungsgebühr

// 贪心思路
var maxProfit = function(prices, fee) {
    let result = 0
    let minPrice = prices[0]
    for(let i = 1; i < prices.length; i++) {
        if(prices[i] < minPrice) {
            minPrice = prices[i]
        }
        if(prices[i] >= minPrice && prices[i] <= minPrice + fee) {
            continue
        }

        if(prices[i] > minPrice + fee) {
            result += prices[i] - minPrice - fee
            // 买入和卖出只需要支付一次手续费
            minPrice = prices[i] -fee
        }
    }
    return result
};

// 动态规划
/**
 * @param {number[]} prices
 * @param {number} fee
 * @return {number}
 */
var maxProfit = function(prices, fee) {
    // 滚动数组
    // have表示当天持有股票的最大收益
    // notHave表示当天不持有股票的最大收益
    // 把手续费算在买入价格中
    let n = prices.length,
        have = -prices[0]-fee,   // 第0天持有股票的最大收益
        notHave = 0;             // 第0天不持有股票的最大收益
    for (let i = 1; i < n; i++) {
        // 第i天持有股票的最大收益由两种情况组成
        // 1、第i-1天就已经持有股票,第i天什么也没做
        // 2、第i-1天不持有股票,第i天刚买入
        have = Math.max(have, notHave - prices[i] - fee);
        // 第i天不持有股票的最大收益由两种情况组成
        // 1、第i-1天就已经不持有股票,第i天什么也没做
        // 2、第i-1天持有股票,第i天刚卖出
        notHave = Math.max(notHave, have + prices[i]);
    }
    // 最后手中不持有股票,收益才能最大化
    return notHave;
};

Guess you like

Origin blog.csdn.net/qq_28838891/article/details/133695578