Liuliu의 회고 검토의 조합 (조합)

"계속 만들고 성장을 가속화하십시오! "너겟 데일리 뉴 플랜 · 6월 업데이트 챌린지" 참여 5일차입니다 . 이벤트 세부 정보를 보려면 클릭하세요. "

머리말

예전에는 샤오류류가 항상 자신의 알고리즘이 상대적으로 나쁘고 단점이 있다고 느꼈는데 예전에는 정말 3일 낚시, 2세트 그물, 며칠 양치질을 하다가 서서히 멈췄습니다. 이 둘째, 플랫폼 활동의 도움으로 천천히 양치질을 시작할 계획이며 양치질 질문을 요약하고 제 생각과 아이디어 등을 이야기하겠습니다. 친구 여러분도 이번 기회에 여러분의 부족한 점을 만회할 수 있습니다.

주제

두 개의 정수 합이 주어지면  범위에서  가능한 모든   숫자 조합을 n 반환  k합니다  .[1, n]k

어떤 순서로든  답변을 반환 할 수 있습니다  .

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

주제 분석

사실 이 질문은 우리 수학의 지식 포인트와 비슷하며 순열과 조합이 비슷합니다. 그러나 이 질문은 철저하지만 for 루프로 해결할 수는 없습니다. n의 크기가 확인되지 않았기 때문에 n=4, k=2라고 가정하면 실제로 이렇게 할 수 있습니다.

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++) {
            }
        }
    }
}
复制代码

그러나 불행히도 n과 k가 매우 크면 아마도 죽고 싶을 것이므로 역추적을 사용하기 때문에 일반적인 쓰기 방법으로 사용할 수 없습니다.

역추적 알고리즘이란

사실 이 문제에 대한 해결책은 역추적인데 역추적이란 무엇일까요?

역추적 방법(탐색 및 역추적 방법)은 휴리스틱 방법이라고도 하는 일종의 최적 탐색 방법으로, 목표를 달성하기 위해 최적의 조건에 따라 앞으로 탐색합니다. 그러나 탐색이 특정 단계에 도달하고 원래의 선택이 최적이 아니거나 목표를 달성하지 못한 것으로 판명되면 한 발 물러서서 새로운 선택을 하게 됩니다. 상태]를 "역추적 지점"이라고 합니다.

역추적 방법에서는 현재 부분 솔루션이 확장될 때마다 선택적 상태 집합에 직면하고 이 집합에서 선택하여 새로운 부분 솔루션을 구성합니다. 이러한 상태 집합의 구조는 다중 포크 트리이며, 각 트리 노드는 가능한 부분 솔루션을 나타내며, 해당 자식은 이를 기반으로 생성된 다른 부분 솔루션입니다. 트리의 루트는 초기 상태이며 이러한 상태 집합을 상태 공간 트리라고 합니다.

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

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

回溯法解题的关键要素

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

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

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

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

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

回溯解决的问题

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 자르기 문제: 특정 규칙에 따라 줄을 자르는 몇 가지 방법이 있습니다.
  • 부분집합 문제: N개의 숫자 집합에 적합한 부분집합이 몇 개나 있습니까?
  • 체스판 문제: N Queens, 스도쿠 풀기 등

따라서 위의 문제를 조합하여 소급적으로 해결하는 것이다.

문제 솔루션의 첫 번째 버전을 살펴보겠습니다.

이미지.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();
        }
    }
}
复制代码

사실, 우리는 가지치기(pruning)라고 부르는 최적화 지점이 실제로 있다는 것을 발견했습니다.

두 번째 판, 가지치기

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();
    }
}
复制代码

가지치기할 위치는 우리가 가로지르는 조건에 있습니다. 이전에는 j <= n이었고 이제는 j <= n-(k-temp.size())+1입니다.

우리가 왜 이것을 할 수 있는지 보자.모두가 생각하는 n=4, k=3을 원하면 실제로 2점에서 횡단하면 뒤쪽에 3과 4가 있기 때문에 가능하지만 그렇습니다. 내 시작 인덱스가 3이면 나중에 무슨 일이 일어나든 그 뒤에는 최대 3, 4이고 k=3에 도달할 수 없기 때문에 깊이 탐색할 필요가 없다는 것이 사실입니까? 그래서 이것은 가지 치기입니다.

선택된 요소의 수: temp.size();

필요한 요소의 수는 다음과 같습니다. k - path.size();

이 시작 위치에서 최대 n개의 집합에서: n - (k - path.size()) + 1, 순회 시작

따라서 가지 치기 조건은 j <= n-(k-temp.size())+1이므로 n=4, k=3을 대입하여 설정할 수 있습니다.

역추적 템플릿

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

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

复制代码

마치다

자, 오늘은 여기까지입니다. 앞으로 며칠 동안 계속해서 백트래킹과 관련된 질문을 공유하고 연습을 강화할 수 있습니다.

рекомендация

отjuejin.im/post/7103143480792186916
рекомендация