Combinaisons (combinaisons) de la revue rétrospective de Liuliu

"Continuez à créer, accélérez la croissance ! C'est le 5ème jour de ma participation au "Nuggets Daily New Plan · June Update Challenge", cliquez pour voir les détails de l'événement "

avant-propos

Avant, Xiao Liuliu avait toujours l'impression que son algorithme était relativement mauvais, et c'était une lacune. Dans le passé, c'était vraiment une pêche de trois jours, deux jeux de filets et quelques jours de brossage, puis s'arrêtait lentement, alors cette seconde, avec l'aide des activités de la plateforme, je prévois de commencer à brosser lentement, et je résumerai également les questions de brossage, parlerai de certaines de mes propres réflexions et de mes propres idées, etc. J'espère que cela pourra être utile à mon amis. Vous pouvez aussi profiter de cette occasion pour combler vos lacunes. J'espère que vous saurez vous y tenir.

sujet

Étant donné deux sommes entières  n ,  krenvoie  [1, n] toutes les  k combinaisons possibles de nombres dans la plage.

Vous pouvez   retourner les réponses dans n'importe quel ordre .

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
复制代码

analyse de sujet

En fait, cette question est un peu comme un point de connaissance dans nos mathématiques, la permutation et la combinaison sont similaires. Mais bien que cette question soit exhaustive, vous ne pouvez pas la résoudre avec une boucle for. Parce que la taille de n n'est pas confirmée, si vous supposez n = 4, k = 2, vous pouvez réellement le faire

int n = 4;
for (int i = 1; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
        for (int u = j + 1; u <= n; n++) {
           for (int f = j + 1; f <= n; f++) {
            }
        }
    }
}
复制代码

Mais malheureusement, nous ne pouvons pas l'utiliser comme une manière générale d'écrire, car lorsque n et k sont très grands, vous voulez probablement mourir, alors utilisez le retour en arrière

Qu'est-ce qu'un algorithme de backtracking

En fait, la solution à ce problème est le retour en arrière, alors qu'est-ce que le retour en arrière ?

La méthode de backtracking (méthode d'exploration et de backtracking) est une sorte de méthode de recherche optimale, également appelée méthode heuristique, qui recherche en avant selon les conditions optimales pour atteindre l'objectif. Cependant, lorsque l'exploration atteint une certaine étape, et qu'il s'avère que le choix initial n'est pas optimal ou ne permet pas d'atteindre l'objectif, elle va prendre du recul et faire un nouveau choix. [état] est appelé le "point de retour en arrière".

Dans la méthode de retour en arrière, chaque fois que la solution partielle actuelle est développée, un ensemble d'états facultatif est confronté et une nouvelle solution partielle est construite en sélectionnant dans cet ensemble. La structure d'un tel ensemble d'états est un arbre à plusieurs fourches, chaque nœud d'arbre représente une solution partielle possible et ses enfants sont d'autres solutions partielles générées sur sa base. La racine de l'arbre est l'état initial, et un tel ensemble d'états est appelé un arbre d'espace d'états.

回溯法对任一解的生成,一般都采用逐步扩大解的方式。每前进一步,都试图在当前部分解的基础上扩大该部分解。它在问题的状态空间树中,从开始结点(根结点)出发,以深度优先搜索整个状态空间。这个开始结点成为活结点,同时也成为当前的扩展结点。在当前扩展结点处,搜索向纵深方向移至一个新结点。这个新结点成为新的活结点,并成为当前扩展结点。如果在当前扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的活结点处,并使这个活结点成为当前扩展结点。回溯法以这种工作方式递归地在状态空间中搜索,直到找到所要求的解或解空间中已无活结点时为止。

回溯法与穷举法有某些联系,它们都是基于试探的。穷举法要将一个解的各个部分全部生成后,才检查是否满足条件,若不满足,则直接放弃该完整解,然后再尝试另一个可能的完整解,它并没有沿着一个可能的完整解的各个部分逐步回退生成解的过程。而对于回溯法,一个解的各个部分是逐步生成的,当发现当前生成的某部分不满足约束条件时,就放弃该步所做的工作,退到上一步进行新的尝试,而不是放弃整个解重来。

回溯法解题的关键要素

确定了问题的解空间结构后,回溯法将从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。开始结点成为活结点,同时也成为扩展结点。在当前的扩展结点处,向纵深方向搜索并移至一个新结点,这个新结点就成为一个新的活结点,并成为当前的扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使其成为当前的扩展结点。回溯法以上述工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。

运用回溯法解题的关键要素有以下三点:

(1) 针对给定的问题,定义问题的解空间;

(2) 确定易于搜索的解空间结构;

(3) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

回溯解决的问题

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • Problème de coupe : Il existe plusieurs manières de couper une ficelle selon certaines règles
  • Problème de sous-ensemble : combien de sous-ensembles éligibles y a-t-il dans un ensemble de N nombres
  • Problèmes d'échiquier : N Queens, résolution de Sudoku, etc.

Par conséquent, la combinaison du problème ci-dessus consiste à le résoudre rétrospectivement.

Jetons un coup d'oeil, la première version de la solution du problème

image.png

public class fourteen {
    public static void main(String[] args) {
        combine(4, 2);
    }


    public static List<List<Integer>> combine(int n, int k) {
        //这个是我用来放结果的
        List<List<Integer>> res = new ArrayList<>();
        //这个是用来方我们走过的路径的,这不好用List,可以用队列,因为我们要回退上一步
        LinkedList<Integer> temp = new LinkedList<>();
        backtracking(1, res, k, temp, n);
        return res;

    }

    private static void backtracking(int i, List<List<Integer>> res, int k, LinkedList<Integer> temp, int n) {
        //这是递归的结束条件
        if (temp.size() == k) {
            res.add(new ArrayList<>(temp));
            return;
        }
        //这里的for循环,表示我们的深入遍历,遍历到最后,然后再一步步回溯,就是把先把每个一个向下走的路走完,再走其他的分叉
        for (int j = i; j <= n; j++) {
            temp.add(j);
            back(j + 1, res, k, temp, n);
            temp.removeLast();
        }
    }
}
复制代码

En fait, nous avons constaté qu'il existe en fait des points d'optimisation, que nous appelons l'élagage.

Deuxième édition, taille

public static List<List<Integer>> combine(int n, int k) {
    //这个是我用来放结果的
    List<List<Integer>> res = new ArrayList<>();
    //这个是用来方我们走过的路径的,这不好用List,可以用队列,因为我们要回退上一步
    LinkedList<Integer> temp = new LinkedList<>();
    backtracking(1, res, k, temp, n);
    return res;

}

private static void backtracking(int i, List<List<Integer>> res, int k, LinkedList<Integer> temp, int n) {
    //这是递归的结束条件
    if (temp.size() == k) {
        res.add(new ArrayList<>(temp));
        return;
    }
    //这里的for循环,表示我们的深入遍历,遍历到最后,然后再一步步回溯,就是把先把每个一个向下走的路走完,再走其他的分叉,减枝的地方就是在 我们遍历的条件,之前是j<=n,现在是j <= n-(k-temp.size())+1
    for (int j = i; j <= n-(k-temp.size())+1; j++) {
        temp.add(j);
        backtracking(j + 1, res, k, temp, n);
        temp.removeLast();
    }
}
复制代码

L'endroit à tailler est dans la condition que nous traversons, avant c'était j <= n, maintenant c'est j <= n-(k-temp.size())+1

Voyons pourquoi on peut faire ça. Tout le monde pense, si on veut n=4, k=3, alors en fait, si on parcourt de 2 points, c'est possible, car il y a 3 et 4 derrière, oui, mais si mon index de départ est 3. Est-il vrai que je n'ai pas besoin de traverser en profondeur, car quoi qu'il arrive plus tard, il n'y a que 4 derrière, au plus 3, 4, et il ne peut pas atteindre k = 3 du tout, donc c'est de l'élagage

Le nombre d'éléments qui ont été sélectionnés : temp.size();

Le nombre d'éléments requis est : k - path.size();

Dans l'ensemble n au plus à partir de cette position de départ : n - (k - path.size()) + 1, commencer à parcourir

Ainsi, la condition d'élagage est j <= n-(k-temp.size())+1, vous pouvez y substituer n=4, k=3 et le configurer

modèle de retour en arrière

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

复制代码

Finir

Bon, c'est tout pour aujourd'hui. Dans les prochains jours, nous pourrons continuer à partager les questions liées au retour en arrière et renforcer la pratique.

Je suppose que tu aimes

Origine juejin.im/post/7103143480792186916
conseillé
Classement