LeetCode: 526. 优美的排列(状态压缩DP)

假设有从 1 到 N 的 N 个整数,如果从这 N 个数字中成功构造出一个数组,使得数组的第 i 位 (1 <= i <= N) 满足如下两个条件中的一个,我们就称这个数组为一个优美的排列。条件:

第 i 位的数字能被 i 整除
i 能被第 i 位上的数字整除
现在给定一个整数 N,请问可以构造多少个优美的排列?

示例1:

输入: 2
输出: 2
解释: 

第 1 个优美的排列是 [1, 2]:
  第 1 个位置(i=1)上的数字是1,1能被 i(i=1)整除
  第 2 个位置(i=2)上的数字是2,2能被 i(i=2)整除

第 2 个优美的排列是 [2, 1]:
  第 1 个位置(i=1)上的数字是2,2能被 i(i=1)整除
  第 2 个位置(i=2)上的数字是1,i(i=2)能被 1 整除
说明:

N 是一个正整数,并且不会超过15。

分析:

       如果说只是解决这个问题的话,应该不难,可以用回溯的方法,在每个位置排列上符合要求的数字,统计答案。这是我的代码:

class Solution {
public:
    int ans = 0;

    void dfs(int k, int n, vector<int> &vis){
        if(k > n){
            ans++;
            return;
        }
        for(int i = 1; i <= n; i++){
            if(!vis[i] && (i % k == 0 || k % i == 0)){
                vis[i] = 1;
                dfs(k + 1, n, vis);
                vis[i] = 0;
            }
        }
    }

    int countArrangement(int n) {
        vector<int> vis(n + 1, 0);
        for(int i = 1; i <= n; i++){
            vis[i] = 1;
            dfs(2, n, vis);
            vis[i] = 0;
        }
        return ans;    
    }
};

        结果如下:

       这里主要是想要学习一下状态压缩 + 动态规划的方法。我们可以用一个数mask的二进制表示来记录1到n被选取的情况,比如mask = 7,那它的二进制位0111,表示数字1,2,3被选取(因为下标从0开始,所以下标分别为0,1,2)。令num = __builtin_popcount(mask),那么num就是mask的二进制表示中1的个数。mask = 7,二进制为0111,那么num = 3。接下来我们定义数组f,f[mask]表示状态为mask时,且选择的数字排在前num个位置,优美排列的方案总数,那么最终返回的结果就应当是f[2^n - 1]。

       然后是状态转移方程,假设mask中有num个数字1,那么mask可以从有num - 1个数字1的状态转移过来。比如mask的二进制表示为110010010,说明选择了2,5,8,9四个数字,我所选的第四个数字可能是2,5,8,9,而符合条件的分别是2(4 % 2 == 0)和8(8 % 4 == 0),所以只能从这两个状态进行转移:把2放在第4位,求5,8,9放在前三位完美排列的方案数,因为5,8,9对应的状态是110010000,即f[(110010000)_{2}];或者把8放在第4位,求2,5,9放在前三位完美排列的方案数,此时状态为100010010,即f[(100010010)_{2}]。

       由此可见我们必然从一个比mask小的状态转移过来,所以我们可以从1开始枚举mask,直到mask = 2 ^ n - 1。至于初始状态f[0],我们在计算只有一个1的mask状态时,需要用到f[0],因为任何数都可以放到1这个位置,所以是一个可行的方案,那么需要我们令f[0] = 1。

class Solution {
public:
    int countArrangement(int n) {
        vector<int> f(1 << n);
        f[0] = 1;
        for(int mask = 1; mask < (1 << n); mask++){
            //mask从1枚举到2 ^ n - 1
            int num = __builtin_popcount(mask); //num表示mask二进制表示中1的个数
            for(int i = 0; i < n; i++){
                //分别判断1到n这些数字是否被选择
                if(mask & (1 << i) && (num % (i + 1) == 0 || (i + 1) % num == 0)){
                    //mask & (1 << i) 大于0,表示mask第i位为1,即数字i + 1被选择
                    //一共有num个数字组成完美排列,最后一个位置为num
                    //num % (i + 1) == 0 || (i + 1) % num == 0即判断i + 1能否放在最后一个位置
                    f[mask] += f[mask ^ (1 << i)];
                    //mask ^ (1 << i)表示将mask第i位置0,从该状态转移到f[mask]
                }
            }
        }
        return f[(1 << n) - 1];
    }
};

       因为我们枚举了2 ^ n个状态,对于每个状态都从1到n进行一次扫描,所以总的时间复杂度为O(n * 2 ^ n)。而这远远优于上面回溯的方法。

       通过本题可以学习到,有些时候可以用二进制的0和1表示不同的状态,通过位运算来提高效率,同时我们在不同的状态之间能够寻找一定的方式,进行状态转移,从而利用动态规划优化我们的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值