526.优美的排列

🏐 526.优美的排列

假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列

  • perm[i] 能够被 i 整除
  • i 能够被 perm[i] 整除

给你一个整数 n ,返回可以构造的 优美排列数量

思路

基本思想

看到题目之后,第一想法就是按照回溯法求出所有的全排列,然后再判断每个全排列是否符合要求,也就是对于当前全排列来说,每个位置是否符合(下标从1开始):

  • perm[i] 能够被 i 整除
  • i 能够被 perm[i] 整除

按照这种思路编写代码之后,会超时,超时的代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
    //使用回溯法找到所有的排列,然后再判断是否合法
    //针对一个排列的所有数,依次判断是否符合任意一个条件即可
    //解法可行,但是超时
    List<Integer> path=new ArrayList<>();
    int res=0;
    boolean[] visited;
    public int countArrangement(int n) {
        visited=new boolean[n];
        help(n);
        return res;
    }
    //回溯法求排列
    private void help(int n){
        //形成了一个全排列,并且每个元素都符合条件
        if(path.size()==n&&isValid(path)){
            ++res;
        }
        //对于当前全排列来说,1-n的所有元素都会尝试一遍,这是超时的主要问题
        //对于当前位置来说,只有部分元素符合条件,我们可以在回溯之前进行处理
        for(int i=1;i<=n;++i){
            //当前元素没访问,加入全排列中
            if(!visited[i-1]){
                visited[i-1]=true;
                path.add(i);
                help(n);
                //回溯
                visited[i-1]=false;
                path.remove(path.size()-1);
            }
        }
    }
    private boolean isValid(List<Integer> path){
        for(int i=0;i<path.size();++i){
            //两个条件都不满足,说明当前排列不满足条件
            if(!((path.get(i))%(i+1)==0||(i+1)%(path.get(i))==0))
                return false;
        }
        return true;
    }
}

上述代码超时的主要原因在于构造全排列的过程中,每一个位置都尝试了1-n的所有数,其实大部分数都是无意义的尝试,如果我们提前告知回溯法对于当前位置来说,哪些元素符合上面的两个条件之一,这样就可以减少两个操作:

  1. 全排列中的每一个位置不再有无意义的尝试
  2. 构造完全排列之后不用在判断全排列的有效性,因为每个位置的元素都是从合法集中选取的

相当于提前构造一个map,每个位置有哪些元素合法提前构造

执行流程

  1. 初始化一个二维List,里面存储每个位置合法的元素
  2. 进行回溯,构造全排列时每个位置的元素选取从合法元素集中选取
  3. 构造完一个全排列结果数就+1
  4. 返回需要的结果

代码

根据以上分析,得出以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
    //使用回溯法找到所有的排列,然后再判断是否合法
    //针对一个排列的所有数,依次判断是否符合任意一个条件即可
    //解法可行,但是超时
    
    int res=0;
    boolean[] visited;
    List<List<Integer>> map=new ArrayList<>();
    public int countArrangement(int n) {
        visited=new boolean[n];
        for(int i=0;i<=n;++i){
            map.add(new ArrayList<>());
        }
        //统计每个位置有哪些元素合法
        for(int i=1;i<=n;++i){
            for(int j=1;j<=n;++j){
                if(j%i==0||i%j==0)
                    //第i个位置上添加元素j
                    map.get(i).add(j);
            }
        }
        help(n,1);
        return res;
    }
    //回溯法求排列
    private void help(int n,int index){
        //形成了一个全排列,并且每个元素都符合条件
        if(index==n+1){
            ++res;
            return;
        }
        //对于当前全排列来说,1-n的所有元素都会尝试一遍,这是超时的主要问题
        //对于当前位置来说,只有部分元素符合条件,我们可以在回溯之前进行处理
        for(int num:map.get(index)){
            if(!visited[num-1]){
                visited[num-1]=true;
                help(n,index+1);
                visited[num-1]=false;
            }
        }
    }
}

总结

解决问题的核心还是使用回溯法求出所有的排列,只不过第一份代码是求出所有的代码,然后进行筛选,相当于每个位置上填充的元素是从1-n中选取的,而这些无意义的尝试会使得算法超时。第二份代码提前进行构造,每个位置上填充的元素是从一个候选List中选取的,这个候选List中的所有元素都是合法的,这既不会有无意义的尝试,在形成全排列的时候又不用担心当前全排列是否合法,相当于一举两得