Soft-Test-Algorithmus - Algorithmus

Erstens: Hintergrund der Geschichte

Vor kurzem bereite ich mich auf die Prüfung zum Software-Ingenieur im Mai vor. Oben haben wir die 8 häufig verwendeten Sortieralgorithmen zusammengefasst. In diesem Artikel nehmen wir eine CD mit verschiedenen anderen Algorithmen, die im Softtest entwickelt wurden. Die in diesen Algorithmen verkörperten Ideen bilden den Kern unserer Studie. Ich hoffe, dass dieser Artikel Ihnen ein tieferes Verständnis dafür vermitteln kann, was ein Algorithmus ist. Schätzen Sie die Feinheiten verschiedener Algorithmen.

Zweitens: Teile und herrsche

2.1 Konzept

Teilen und erobern bedeutet, wie der Name schon sagt, Teilen und Erobern. Dabei wird ein komplexes Problem in zwei oder mehr identische oder ähnliche Teilprobleme aufgeteilt und die Teilprobleme dann in kleinere Teilprobleme aufgeteilt, bis die endgültigen Teilprobleme einfach und direkt gelöst werden können und die Lösung des ursprünglichen Problems die Kombination der Lösungen der Teilprobleme ist.

2.2 Beschreibung des Themas

Wir verwenden die Idee der Verzweigung, um in einem geordneten Array nach einem bestimmten Wert zu suchen. Beispiel:
Suchen Sie im folgenden Array nach einem Array mit dem Wert 28
Fügen Sie hier eine Bildbeschreibung ein

2.3 Code-Implementierung

    public static void main(String[] args) {
    
    
        int [] nums ={
    
    8,20,28,74,92,188,372,500};
        int targetNumber = 28;
        int i = binarySearch(targetNumber, 0, nums.length, nums);
        System.out.println(i);
    }
    
        /**
     * 二分查找
     *
     * @param targetNumber 要查找的目标数字
     * @param beginIndex 查找区间的起始下标
     * @param endIndex 查找区间的结束下标
     * @param nums 给定的已排序数组
     * @return 如果找到目标数字,返回目标数字的下标;否则返回 -1
     */
    public static int binarySearch(int targetNumber, int beginIndex, int endIndex, int[] nums) {
    
    
        // 计算中间位置
        int middleIndex = (beginIndex + endIndex) / 2;
        // 如果找到目标数字,返回目标数字的下标
        if (targetNumber == nums[middleIndex]) {
    
    
            return middleIndex;
        }
        // 如果目标数字比中间位置的数大,说明目标数字在右半区间
        if (targetNumber > nums[middleIndex]) {
    
    
            return binarySearch(targetNumber, middleIndex, endIndex, nums);
        }
        // 如果目标数字比中间位置的数小,说明目标数字在左半区间
        return binarySearch(targetNumber, beginIndex, middleIndex, nums);
    }

2.4 Zusammenfassung und Verbesserung

Unser Code implementiert einen klassischen binären Suchalgorithmus unter Verwendung einer rekursiven Suche. Nachfolgend eine ausführliche Erläuterung:

  • In der Hauptmethode werden ein Array „Nums“ und eine Zielnummer „TargetNumber“ definiert. Anschließend wird die Methode „binarySearch“ aufgerufen, um die Suchergebnisse zu suchen und auszugeben.
  • Die Methode „binarySearch“ ist eine rekursive Funktion, um die Zielnummer „targetNumber“ im sortierten Array „nums“ zu finden. beginIndex und endIndex in den Funktionsparametern stellen den Startindex und Endindex des Suchintervalls dar, und nums stellt das angegebene sortierte Array dar.
  • Berechnen Sie zunächst den MiddleIndex der mittleren Position. Wenn die Zielnummer gefunden wird, geben Sie den tiefgestellten MiddleIndex der Zielnummer zurück.
  • Wenn die Zielzahl größer als die Zahl an der mittleren Position ist, bedeutet dies, dass sich die Zielzahl in der rechten Hälfte des Intervalls befindet. Rufen Sie die Methode BinarySearch rekursiv auf, ändern Sie den Startindex des Suchintervalls in middleIndex und lassen Sie den Endindex unverändert.
  • Wenn die Zielzahl kleiner als die Zahl an der mittleren Position ist, bedeutet dies, dass sich die Zielzahl in der linken Hälfte des Intervalls befindet. Rufen Sie rekursiv die Methode „binarySearch“ auf, ändern Sie den Endindex des Suchintervalls in „MiddleIndex“ und der Startindex bleibt unverändert.
  • Gibt -1 zurück, wenn die Zielnummer nicht gefunden wird.

Die zeitliche Komplexität dieses Algorithmus beträgt O(log n), wobei n die Länge des Arrays ist. Dies liegt daran, dass bei jeder Suche das Suchintervall um die Hälfte reduziert wird und das Protokoll höchstens n-mal durchsucht werden muss. Dieser Algorithmus ist ein effizienter Suchalgorithmus, der häufig für Suchvorgänge in sortierten Arrays verwendet wird.

Drittens: Zurückverfolgen

3.1 Konzept

  • Mit der Backtracking-Methode kann systematisch nach einer vollständigen oder beliebigen Lösung eines Problems gesucht werden.
  • Beim Backtracking handelt es sich typischerweise um eine Tiefensuche eines Problemzustands, bei der der Algorithmus versucht, Schritt für Schritt eine Lösung zu entwickeln, wobei jede Entscheidung den Problemzustand zum nächsten Schritt verschiebt und überprüft, ob der aktuelle Zustand das Problem erfüllt. Wenn der aktuelle Status die Problemanforderungen erfüllt, suchen Sie weiter nach unten. Wenn nicht, kehren Sie zum vorherigen Status zurück und versuchen Sie es mit anderen Entscheidungen.

3.2 Beschreibung des Themas

  • Geben Sie bei gegebenem Wurzelknoten eines Binärbaums alle Pfade vom Wurzelknoten zu den Blattknoten in beliebiger Reihenfolge zurück.

  • Ein Blattknoten ist ein Knoten, der keine untergeordneten Knoten hat.
    Fügen Sie hier eine Bildbeschreibung ein

    Eingabe: root = [1,2,3,4,5,6,7,8,9]
    Ausgabe: ["1->2->4->8", "1->2->4->9", "1->2->5", "1->3->6", "1->3->7"]

Hinweis:

树中节点的数目在范围 [1, 100] 内
-100 <= Node.val <= 100

3.3 Code-Implementierung

Unsere Code-Implementierung verwendet Tiefensuch- und Backtracking-Methoden.

  • Zunächst wird ausgehend vom Wurzelknoten eine Tiefendurchquerung des Binärbaums durchgeführt.
  • Verwenden Sie den StringBuilder StringBuilder , um den aktuellen Pfad während des Durchlaufs aufzuzeichnen
  • Immer wenn ein Blattknoten durchlaufen wird, wird der aktuelle Pfad zur Ergebnisliste hinzugefügt und die vorherige Knotenebene wird zurückverfolgt.

3.3.1 TreeNode-Klasse

public class TreeNode {
    
    
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) {
    
     val = x; }
}

3.3.2 Verarbeiten Sie das Array in eine binäre Baumstruktur und geben Sie den Wurzelknoten zurück

public static TreeNode constructTree(int[] nums) {
    
    
    if (nums == null || nums.length == 0) {
    
    
        return null;
    }
    // 创建根节点
    TreeNode root = new TreeNode(nums[0]);  
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    int i = 1;
    while (i < nums.length) {
    
    
        // 取出队头元素
        TreeNode parent = queue.poll();  
        if (i < nums.length && nums[i] != null) {
    
    
            // 创建左子节点
            parent.left = new TreeNode(nums[i]);  
            queue.offer(parent.left);
        }
        i++;
        if (i < nums.length && nums[i] != null) {
    
    
            // 创建右子节点
            parent.right = new TreeNode(nums[i]);  
            queue.offer(parent.right);
        }
        i++;
    }
    return root;
}

3.3.3 Durchführen einer Suche

class Solution {
    
    
    public List<String> binaryTreePaths(TreeNode root) {
    
    
        // 用于保存所有从根节点到叶子节点的路径
        List<String> result = new ArrayList<String>();  
        // 处理特殊情况,如果根节点为空,则返回空路径列表
        if (root == null) {
    
      
            return result;
        }
        // 开始深度优先搜索
        dfs(root, new StringBuilder(), result);  
        return result;
    }

    private void dfs(TreeNode node, StringBuilder path, List<String> result) {
    
    
        // 如果当前节点为空,返回
        if (node == null) {
    
      
            return;
        }
        // 保存当前路径的长度
        int len = path.length();  
        // 如果当前节点是叶子节点,则将当前路径添加到结果列表中
        if (node.left == null && node.right == null) {
    
      
            result.add(path.append(node.val).toString());
            // 恢复当前路径的长度
            path.setLength(len);  
            return;
        }
        // 将当前节点添加到路径中,并加上箭头
        path.append(node.val).append("->");  
        // 递归遍历左子树
        dfs(node.left, path, result);
        // 递归遍历右子树  
        dfs(node.right, path, result);
        // 恢复当前路径的长度  
        path.setLength(len);  
    }
}

3.4 Zusammenfassung und Verbesserung

  • In diesem Beispiel müssen wir den aktuellen Knoten zum aktuellen Pfad hinzufügen
  • Durchlaufen Sie dann rekursiv den linken und rechten Teilbaum des Knotens
  • Beim Backtracking muss der aktuelle Knoten aus dem aktuellen Pfad gelöscht und gleichzeitig andere Zweige ausgewählt werden, um mit der Suche fortzufahren, bis alle Pfade gefunden sind.
  • Dieses Problem verkörpert die Kernidee des Backtracking-Algorithmus, der darin besteht, den Lösungsraum des Problems durch Versuch und Irrtum zu durchsuchen.

Viertens: Backtracking – das Problem der Königin

4.1 Konzept

  1. Nach den Schachregeln kann eine Dame eine Figur angreifen, die sich in derselben Reihe, Spalte oder auf derselben Diagonale wie die Dame befindet.
  2. Das n-Damen-Problem untersucht, wie man n Damen auf einem n×n-Schachbrett platziert und dafür sorgt, dass die Damen sich nicht gegenseitig angreifen können.

4.2 Zeichnungsdarstellung

Beispiel einer realisierbaren Lösung:
Fügen Sie hier eine Bildbeschreibung ein
Beispiel einer Prozesskonstruktion:
Fügen Sie hier eine Bildbeschreibung ein

4.3 Code-Implementierung

4.3.1 Umsetzungsideen

Makro:
Verwenden Sie die Tiefensuchmethode, um zu prüfen, ob jede Position die Bedingungen in der Reihenfolge der ersten Zeile und der zweiten Spalte erfüllt.
Mikro:
Definieren Sie ein zweidimensionales Array zur Darstellung des Schachbretts, definieren Sie eine Variable n zur Darstellung mehrerer Damen, definieren Sie
eine Methode, um zu bestimmen, ob die aktuell platzierte Dame mit der vorherigen Dame in Konflikt steht (gleiche Spalte, oben links, oben rechts), und der Konflikt gibt 0 zurück; andernfalls wird 1 zurückgegeben, was angibt, dass die Dame an dieser Position platziert werden kann.
Definieren Sie eine rekursive Funktion, die versucht, eine Königin in der aktuellen Zeile zu platzieren.

4.3.2 Spezifischer Code

package com.lsn.NQueen;

public class NQueens {
    
    

    // 定义一个二维数组表示棋盘
    int[][] board;

    // 定义一个变量表示几个皇后
    int n;

    // 构造函数,初始化棋盘和n
    public NQueens(int n) {
    
    
        board = new int[n][n];
        this.n = n;
    }

    // 判断当前摆放的皇后是否与之前的皇后冲突
    public boolean isSafe(int row, int col) {
    
    
        int i, j;

        // 检查当前列是否有皇后
        for (i = 0; i < row; i++) {
    
    
            if (board[i][col] == 1) {
    
    
                return false;
            }
        }

        // 检查左上方是否有皇后
        for (i = row, j = col; i >= 0 && j >= 0; i--, j--) {
    
    
            if (board[i][j] == 1) {
    
    
                return false;
            }
        }

        // 检查右上方是否有皇后
        for (i = row, j = col; i >= 0 && j < n; i--, j++) {
    
    
            if (board[i][j] == 1) {
    
    
                return false;
            }
        }

        // 如果都没有冲突,则返回true
        return true;
    }

    // 递归函数,尝试在当前行放置皇后
    public boolean solve(int row) {
    
    
        // 如果所有行都已经摆放完毕,则返回true(终止条件,收集结果)
        if (row == n) {
    
    
            return true;
        }

        // 尝试在当前行的每一列放置皇后(单层逻辑,处理节点)
        for (int col = 0; col < n; col++) {
    
    
            // 判断当前位置是否安全
            if (isSafe(row, col)) {
    
    
                // 如果安全,则将皇后放置在当前位置
                board[row][col] = 1;

                // 递归调用solve函数,尝试在下一行放置皇后
                if (solve(row + 1)) {
    
    
                    return true;
                }

                // 如果下一行无法放置皇后,则回溯到当前行,重新尝试放置皇后(撤销处理节点的情况)
                board[row][col] = 0;
            }
        }

        // 如果当前行的每一列都无法放置皇后,则返回false
        return false;
    }

    // 打印棋盘
    public void printBoard() {
    
    
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                System.out.print(board[i][j] + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
    
    
        // 创建一个NQueens对象,并初始化规模为8
        NQueens nQueens = new NQueens(3);

        // 调用solve函数,尝试解决N皇后问题
        if (nQueens.solve(0)) {
    
    
            // 如果找到了解,则打印棋盘
            nQueens.printBoard();
        } else {
    
    
            // 如果没有找到解,则打印无解
            System.out.println("无解.");
        }
    }
}

Mit dem obigen Code können wir nach einer realisierbaren Lösung für die Königin suchen.

4.4 Zusammenfassung und Verbesserung

Das Königinnenproblem ist auch eine Manifestation der Backtracking-Methode. Es verwendet die Backtracking-Methode, um die Effizienz der Verzweigungsreduzierungsstrategie zu verbessern. Wenn die Bedingungen nicht erfüllt sind, ist keine Rekursion nach unten erforderlich, was die Effizienz des Algorithmus erheblich verbessert. Verglichen mit dem oben angegebenen Beispiel der Binärbaum-Backtracking-Methode ist es komplizierter und spiegelt eine zweidimensionale Suche wider, aber seine Kernidee ist das Backtracking.

Fünftens: Gieriges Gesetz

5.1 Konzept

Treffen Sie immer die Wahl, die Ihnen im Moment am besten erscheint. Das heißt, ohne Berücksichtigung der Gesamtoptimalität ergibt sich in gewissem Sinne nur eine lokal optimale Lösung.

  • Die Greedy-Methode umfasst: den Djikstra-Algorithmus (Dijkstra), den Prim-Algorithmus (Prim) und den Kruskal-Algorithmus (Kruskal) zum Aufbau eines minimalen Spannbaums
  • Der Zweck des Algorithmus besteht darin, eine große Aufgabe in mehrere kleine Aufgaben aufzuteilen und diese einzeln zu lösen, um den Effekt der Gesamtaufgabe vor der Aufteilung zu vervollständigen.
  • Algorithmusprozess
  1. Teilen Sie das zu lösende Problem in mehrere Teilprobleme auf
  2. Lösen Sie jedes Teilproblem, um eine lokal optimale Lösung für das Teilproblem zu erhalten
  3. Synthetisieren Sie die lokalen optimalen Lösungen der Teilprobleme zu einer Lösung des ursprünglichen Problems
  • Das Problem mit diesem Algorithmus
  1. Es gibt keine Garantie dafür, dass die endgültige Lösung optimal ist
  2. Es kann nur der Bereich möglicher Lösungen gefunden werden, die bestimmte Einschränkungen erfüllen

5.2 Zeichnungsdarstellung

Fügen Sie hier eine Bildbeschreibung ein
Fügen Sie hier eine Bildbeschreibung ein

5.3 Code-Implementierung

5.3.1 Titelbeschreibung

Ermitteln Sie bei einem gegebenen Array von Ganzzahlen die maximale Summe eines kontinuierlichen Subarrays (das Subarray enthält mindestens ein Element) und geben Sie die maximale Summe zurück.
Hinweis: Ein Subarray ist ein zusammenhängender Teil eines Arrays.

5.3.2 Java-Code

public class MaximumSubarray {
    
    
    public int maxSubArray(int[] nums) {
    
    
        int currentSum = 0;  // 当前子数组的和
        int maxSum = Integer.MIN_VALUE;  // 最大子数组的和

        // 遍历数组中的每个元素
        for (int num : nums) {
    
    
            // 如果当前子数组的和加上当前元素小于当前元素本身,那么当前子数组的和不再对后续子数组产生正向影响,重新开始一个新的子数组
            currentSum = Math.max(num, currentSum + num);
            // 更新最大子数组的和
            maxSum = Math.max(maxSum, currentSum);
        }

        return maxSum;
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
     -2, 1, -3, 4, -1, 2, 1, -5, 4 };
        MaximumSubarray solution = new MaximumSubarray();
        int maxSum = solution.maxSubArray(nums);
        System.out.println("最大和为:" + maxSum);
    }
}

Im obigen Code verwenden wir die Idee der Greedy-Methode, um vom ersten Element des Arrays zu durchlaufen und die aktuelle Summe des aktuellen Unterarrays und die maximale Summe des größten Unterarrays zu berechnen. Bei jedem Durchlauf aktualisieren wir den Wert von currentSum mit Math.max(num, currentSum + num), wodurch sichergestellt wird, dass currentSum immer die Summe des größten Subarrays ist, das mit dem aktuellen Element endet. Gleichzeitig verwenden wir Math.max(maxSum, currentSum), um den Wert von maxSum zu aktualisieren und sicherzustellen, dass die endgültige maxSum die größte Sub-Array-Summe im gesamten Array ist.

5.4 Zusammenfassung und Verbesserung

  • Die Greedy-Methode ist eine Strategie zur Lösung von Problemen. Ihre Kernidee besteht darin, in jedem Auswahlschritt die optimale Wahl im aktuellen Zustand zu übernehmen, um zu hoffen, schließlich die globale optimale Lösung zu erreichen. Die Greedy-Methode eignet sich normalerweise für Probleme, die die Eigenschaften „optimale Unterstruktur“ und „Greedy Selection“ erfüllen.

  • Die Idee, die Greedy-Methode zusammenzufassen, lässt sich wie folgt zusammenfassen:

  1. Optimale Unterstruktur: Die optimale Lösung des Problems enthält die optimale Lösung der Teilprobleme. Dies bedeutet, dass durch Auswahl der aktuellen optimalen Lösung das ursprüngliche Problem in kleinere Teilprobleme aufgeteilt werden kann und die optimale Lösung jedes Teilproblems zur optimalen Lösung des ursprünglichen Problems kombiniert werden kann.

  2. Greedy-Choice-Eigenschaft (Greedy-Choice-Eigenschaft): Treffen Sie in jedem Auswahlschritt die aktuelle optimale Wahl, dh die lokale optimale Lösung, und hoffen Sie, durch die lokale optimale Lösung die globale optimale Lösung zu erreichen. Dies bedeutet, dass die Greedy-Methode frühere Entscheidungen nicht rückgängig macht oder rückgängig macht, sondern Entscheidungen auf der Grundlage der aktuellen Situation trifft.

  3. Nicht-Regression (unabhängig von Nachwirkungen): Sobald die durch die Greedy-Methode getroffene Auswahl bestimmt ist, kann sie nicht mehr geändert werden und die vorherige Auswahl wird nicht geändert. Es achtet nur auf die optimale Wahl im aktuellen Zustand und glaubt, dass die optimale Wahl bei jedem Schritt das Gesamtoptimum erreichen kann.

  • Dabei ist zu beachten, dass die Greedy-Methode nicht für alle Probleme geeignet ist, da manche Probleme nicht optimal auf Greedy-Methode gelöst werden können. Bei Verwendung der Greedy-Methode sind strenge Ableitungen und Beweise erforderlich, um deren Richtigkeit sicherzustellen.

  • Der Vorteil der Greedy-Methode besteht darin, dass sie einfach und effizient ist und in der Regel in kurzer Zeit eine annähernd optimale Lösung erhalten werden kann. Der Nachteil besteht jedoch darin, dass es keine Garantie dafür gibt, dass die globale optimale Lösung erhalten werden kann, sodass in einigen Fällen suboptimale Lösungen oder falsche Ergebnisse erzielt werden können.

Alles in allem baut die gierige Methode schrittweise die Lösung des Problems auf, indem sie die aktuell optimale Lösung auswählt, in der Hoffnung, durch die lokale optimale Lösung die globale optimale Lösung zu erreichen. Es handelt sich um eine einfache und effiziente Strategie zur Lösung von Problemen, die jedoch eine sorgfältige Analyse der Art und Merkmale des Problems erfordert, um das Vorhandensein gieriger Entscheidungen und optimaler Unterstrukturen festzustellen.

Sechs: Dynamische Programmierung

6.1 Konzept

Das zu lösende Problem wird in mehrere Teilprobleme unterteilt, und die Teilprobleme werden in der Reihenfolge der Teilung gelöst. Die Lösung des vorherigen Teilproblems liefert nützliche Informationen (optimale Unterstruktur) für die Lösung des letzteren Teilproblems. Listen Sie bei der Lösung eines Teilproblems verschiedene mögliche lokale Lösungen auf, behalten Sie die lokalen Lösungen bei, die durch Entscheidungsfindung das Optimum erreichen können, und verwerfen Sie andere lokale Lösungen. Lösen Sie nacheinander jedes Teilproblem und finden Sie schließlich die optimale Lösung für das ursprüngliche Problem

6.2 Zeichnungsdarstellung

Hier ist ein Beispiel für Fibonacci-Zahlen
Fügen Sie hier eine Bildbeschreibung ein
Fügen Sie hier eine Bildbeschreibung ein

6.3 Code-Implementierung

6.3.1 Umsetzungsideen

  1. Definieren Sie eine erforderliche Fibonacci-Zahl
  2. Bestimmen Sie, ob die angeforderte Fibonacci-Zahl kleiner oder gleich 1 ist. Wenn ja, geben Sie die angeforderte Fibonacci-Zahl direkt zurück. Wenn nicht, erstellen Sie ein Array, um die Lösung des Unterproblems aufzuzeichnen
  3. Initialisieren Sie den ersten Fibonacci-Wert und den zweiten Fibonacci-Wert. Addieren Sie bei der Berechnung des dritten Fibonacci-Werts die Werte der ersten beiden Indizes, um den dritten Fibonacci-Wert zu erhalten. Auf diese Weise erhält man schließlich den erforderlichen Fibonacci-Wert

6.3.2 Spezifischer Code

public class Fibonacci {
    
    

    public static void main(String[] args) {
    
    
        int numberArr = 10; // 求第10个斐波那契数
        int result = fibonacci(numberArr); // 第10个斐波那契数的值
        System.out.println("第10个斐波那契数值为:"+ result); // 输出第10个斐波那契数的值
    }
    /**
     * 斐波那契数列的动态规划算法
     * @paramn  第numberArr个斐波那契数,dp为数据处理后的值
     * @return 第numberArr个斐波那契数的值
     */
    public static int fibonacci(int numberArr) {
    
    
        if (numberArr <= 1) {
    
    
            // 当numberArr小于等于1时,直接返回numberArr
            return numberArr;
        }

        //创建数组,用于记录子问题的解
        //dp为数据处理后的值
        int[] dp = new int[numberArr + 1];
        dp[0] = 0; // 初始化第一个数
        dp[1] = 1; // 初始化第二个数

        // 递推求解子问题
        for (int i = 2; i <= numberArr; i++) {
    
    
            //dp[i]:i为该求下标
            //dp[i-1]:i为该求下标-1的下标的值
            //dp{[i-2]:i为该求下标-2的下标的值
            //算出两个下标的值后进行相加,最后得出该求下标的值
            dp[i] = dp[i - 1] + dp[i - 2];
            System.out.println("第"+i+"轮numberArr的值为"+ dp[i]);
        }
        // 返回第numberArr个斐波那契数的值
        return dp[numberArr];
    }
}

6.4 Zusammenfassung und Verbesserung

Dynamische Programmierung (Dynamic Programming) ist eine Methode, komplexe Probleme in kleinere Teilprobleme zu zerlegen und diese rekursiv zu lösen. Es ist allgemein auf Probleme mit den Eigenschaften „optimale Unterstruktur“ und „überlappende Teilprobleme“ anwendbar.

Eine kurze Zusammenfassung der Idee der dynamischen Programmiermethode lässt sich wie folgt zusammenfassen:

  1. Optimale Unterstruktur: Die optimale Lösung eines Problems kann aus optimalen Lösungen für Teilprobleme konstruiert werden. Dies bedeutet, dass die Lösung des ursprünglichen Problems aus den Lösungen verwandter Teilprobleme kombiniert werden kann.

  2. Überlappende Teilprobleme: Während des rekursiven Lösungsprozesses werden viele Teilprobleme mehrmals neu berechnet. Bei der dynamischen Programmierung werden Speicher- oder Bottom-up-Methoden verwendet, um die Lösungen von Teilproblemen zu speichern, um wiederholte Berechnungen zu vermeiden und die Effizienz zu verbessern.

  3. Zustandsübergangsgleichung (Zustandsübergangsgleichung): Die dynamische Programmierung beschreibt den Lösungsprozess des Problems durch Definieren des Zustands und der Übergangsgleichung zwischen Zuständen. Die Zustandsübergangsgleichung stellt die Beziehung zwischen dem aktuellen Zustand und dem vorherigen Zustand des Problems dar, und die optimale Lösung wird durch die Zustandsübergangsgleichung abgeleitet.

  4. Bottom-up-Berechnungsreihenfolge: Die dynamische Programmierung verwendet normalerweise eine Bottom-up-Berechnungsreihenfolge, beginnend mit dem kleinsten Unterproblem und schrittweiser Lösung, bis die Lösung des ursprünglichen Problems abgeleitet ist. Dadurch wird sichergestellt, dass beim Lösen alle Teilprobleme gelöst wurden.

  5. Der Vorteil der dynamischen Programmierung besteht darin, dass sie komplexe Probleme lösen und optimale Lösungen erhalten kann, wodurch wiederholte Berechnungen vermieden und die Recheneffizienz verbessert werden. Der Nachteil der dynamischen Programmierung besteht jedoch darin, dass sie viel Platz zum Speichern von Zwischenergebnissen benötigt und manchmal eine gewisse Platzkomplexität im Austausch für die Zeitoptimierung opfert.

Zusammenfassend lässt sich sagen, dass die dynamische Programmierung komplexe Probleme in kleinere Teilprobleme zerlegt und diese rekursiv löst, indem sie die Eigenschaften einer optimalen Unterstruktur und überlappender Teilprobleme nutzt und optimale Lösungen durch Zustandsübergangsgleichungen ableitet. Es ist eine häufig verwendete Methode zur Lösung von Optimierungsproblemen und kann eine Vielzahl von Problemen effizient lösen.

Sieben: Dynamische Programmierung – 0/1-Rucksackproblem

7.1 Konzept

7.1.1 Beispiele

Veranschaulichen Sie das 0/1-Rucksackproblem anhand eines Beispiels:
Es gibt vier Gegenstände und die Gesamtkapazität des Rucksacks des Diebes beträgt 8. Wie kann ich den wertvollsten Gegenstand stehlen?
Artikelnummer: 1 2 3 4
Gegenstandsgewicht: 2 3 4 5
Gegenstandswert: 3 4 5 8

7.1.2 Einschränkungen

Verschiedene Einschränkungen lösen das Problem:

  • Partielles Rucksackproblem: Alle Gegenstände sind unterteilbar, das heißt, es ist erlaubt, einen Teil (z. B. 1/3) eines Gegenstandes in den Rucksack zu stecken;
  • 0-1 Rucksackproblem: Alle Gegenstände können nicht weiter aufgeteilt werden, entweder das Ganze in den Rucksack stecken oder aufgeben, und die Situation „nur 1/3 der Gegenstände in den Rucksack auswählen“ ist nicht zulässig;
  • Vollständiges Rucksackproblem: Es gibt keine Begrenzung für die Menge jedes Artikels und derselbe Artikel kann in mehrere Rucksäcke geladen werden.

7.2 Zeichnungsdarstellung

Fügen Sie hier eine Bildbeschreibung ein

7.3 Code-Implementierung

    public static void main(String[] args) {
    
    

        String[] nameArr = {
    
    "鞋子", "音响", "电脑"};

        // 商品重量数组
        int[] weightArr = {
    
    1/*鞋子*/, 4/*音响*/, 3/*电脑*/};

        // 商品价格数组
        int[] priceArr = {
    
    1500/*鞋子*/, 3000/*音响*/, 2000/*电脑*/};

        // 背包容量
        int packageCapacity = 4;

        backpackWithoutRepeat(nameArr, weightArr, priceArr, packageCapacity);
    }



    private static void backpackWithoutRepeat(String[] nameArr, int[] weightArr, int[] priceArr, int packageCapacity) {
    
    
        /**
         * 声明一个能装入 0、1、2、3磅......的背包的二维价格表;举例:就好比 v数组是表2的数据
         */
        int[][] nameBackpack = new int[nameArr.length + 1][packageCapacity + 1];

        // 构建可能装入背包的二维数组
        // 值为0时说明不会装进背包, 值为1说明可能装入背包
        int[][] contentArr = new int[nameArr.length + 1][packageCapacity + 1];

        /**
         * 为什么i一开始是1不是0?看表2的数据,是不是第一行全是0啊
         */
        for (int i = 1; i < nameBackpack.length; i++) {
    
    

            /**
             * 为什么j一开始是1不是0?看表2的数据,是不是第一列全是0啊
             */
            System.out.println(nameBackpack[i]);
            for (int j = 1; j < nameBackpack[i].length; j++) {
    
    

                /**
                 * 文章中当 w[i] > j 时,就有 nameBackpack[i][j] = nameBackpack[i-1][j];
                 * 因为我们程序i是从1开始的,因此原来公式中的w[i]修改成w[i-1];
                 * 当前商品 > 背包容量, 取同列上一行数据
                 */
                if (weightArr[i - 1] > j) {
    
    
                    nameBackpack[i][j] = nameBackpack[i - 1][j];
                } else {
    
    
                    /**
                     *  当前商品 <= 背包容量, 对两部分内容进行比较;
                     *  第一部分, 该列上一行数据
                     */
                    int onePart = nameBackpack[i - 1][j];

                    /**
                     * 还记得文章中写的 当j >= w[i] 时,有 nameBackpack[i][j]=max{nameBackpack[i-1][j],nameBackpack[i-1][j-w[i]]+nameBackpack[i]} 这个公式成立吗?
                     * priceArr[i - 1]: 当前商品价格;
                     * w[i - 1]: 当前商品重量;
                     * j - w[i - 1]: 去掉当前商品, 背包剩余容量;
                     * 不可重复: nameBackpack[i - 1][j - w[i - 1]]: 在上一行, 取剩余重量下的价格最优解;
                     */
                    int otherPart = priceArr[i - 1] + nameBackpack[i - 1][j - weightArr[i - 1]];

                    /**
                     *  取最大值为当前位置的最优解
                     */
                    nameBackpack[i][j] = Math.max(onePart, otherPart);

                    /**
                     *  如果最优解包含当前商品, 则表示当前商品已经被使用, 进行记录
                     */
                    if (otherPart == nameBackpack[i][j]) {
    
    
                        contentArr[i][j] = 1;
                    }
                }
            }
        }


        // 不能重复的场景中
        // 如果该位置的标志位为1, 说明该商品参与了最终的背包添加
        // 如果该位置的标志位为0, 即使该位置的价格为最大价格, 也是从其他位置引用的价格
        // 因为不能重复, 所以每行只取一个数据参与最终计算, 并只判断在最大位置该商品是否参与
        // 该最大位置会随着已经遍历出其他元素而对应不断减小, 直到为0


        // 二维数组最后一个元素必然是最大值, 但是需要知道该最大值是自身计算的 还是比较后引用其他的
        int totalPrice = 0;
        // 最大行下标数, 即商品数
        int maxLine = contentArr.length - 1;
        // 最大列下标数, 即重量
        int maxColumn = contentArr[0].length - 1;
        for (;maxLine > 0 && maxColumn > 0;) {
    
    
            // 等于1表示在该位置该商品参与了计算
            if (contentArr[maxLine][maxColumn] == 1) {
    
    
                // 遍历后, 对重量减少, 下一次从剩余重量中取参与商品
                maxColumn -= weightArr[maxLine - 1];
                totalPrice += priceArr[maxLine - 1];
                System.out.println(nameArr[maxLine - 1] + "加入了背包");
            }
            // 因为不能重复
            // 所以如果该商品参与了背包容量, 则肯定剩余的最大位置处参与,
            // 否则跟该数据无关, 直接跳过
            maxLine--;
        }
        System.out.println("背包可容纳的最大价值: " + totalPrice);
    }


7.4 Zusammenfassung Verbesserung

Die Idee der dynamischen Programmierung kann die folgenden Schritte verwenden, um das 01-Rucksackproblem zu lösen:

  • Definieren Sie den Zustand: Lassen Sie dp[i][j] den maximalen Gesamtwert der ersten i Elemente darstellen, wenn die Rucksackkapazität j beträgt.
  • Initialisierung: Initialisieren Sie das dp-Array auf 0, d. h. dp[i][j]=0, wobei 0≤i≤N, 0≤j≤W.
  • Zustandsübergangsgleichung: Für den i-ten Gegenstand gibt es zwei Möglichkeiten: in den Rucksack stecken oder nicht in den Rucksack stecken.
  • Wenn wi > j, das heißt, das Gewicht des aktuellen Artikels ist größer als die Kapazität des Rucksacks und kann nicht in den Rucksack gesteckt werden, dann ist dp[i][j] = dp[i-1][j], das heißt, der Maximalwert, wenn der Artikel nicht platziert wird, ist derselbe wie bei den vorherigen i-1-Artikeln.
  • Wenn wi ≤ j, das heißt, das Gewicht des aktuellen Gegenstands ist kleiner oder gleich der Rucksackkapazität, können Sie wählen, ob Sie ihn in den Rucksack stecken möchten oder nicht. Vergleichen Sie in beiden Fällen den Maximalwert und nehmen Sie den größeren Wert:
  • Legen Sie es in den Rucksack: dp[i][j] = dp[i-1][j-wi] + vi, dh der Wert des aktuellen Elements plus der Maximalwert, wenn die Kapazität der vorherigen i-1-Elemente j-wi beträgt.
  • Nicht in den Rucksack gelegt: dp[i][j] = dp[i-1][j], dh der Maximalwert, wenn der aktuelle Gegenstand nicht abgelegt wird.
  • Durchlaufberechnung: Verwenden Sie Doppelschleifen, um Gegenstände und Rucksackkapazität zu durchqueren, und aktualisieren Sie den Wert des dp-Arrays gemäß der Zustandsübergangsgleichung.
  • Ergebnisausgabe: Der endgültige maximale Gesamtwert ist dp[N][W], wobei N die Anzahl der Artikel und W die Kapazität des Rucksacks ist.

Zusammenfassung und Verbesserung

Durch das Studium von Algorithmen ist es von Vorteil, unser logisches Denken zu trainieren und die Entwicklung anzuleiten. Ich hoffe, dass dieser Blog es jedem ermöglichen kann, diese klassischen Algorithmen zu lernen, die darin enthaltenen Ideen zu schätzen und sie in die Praxis umzusetzen.

Guess you like

Origin blog.csdn.net/hlzdbk/article/details/130758733