假设有从 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[];或者把8放在第4位,求2,5,9放在前三位完美排列的方案数,此时状态为100010010,即f[]。
由此可见我们必然从一个比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表示不同的状态,通过位运算来提高效率,同时我们在不同的状态之间能够寻找一定的方式,进行状态转移,从而利用动态规划优化我们的算法。