题目描述
如果一个1~n的排列p,对于每个i=1,2,3,…n,都满足i和p[i] 其中一个能整除另一个,则称p是好的。
输入n(1≤n≤15),输出有多少个1~n的排列是好的。
输入样例1
2
输入样例1
2
输入样例2
1
输入样例2
1
算法
状压DP
这么小的数据范围,直接状压DP
来进行计数。用一个n位二进制数mask来表示1~n中数字的使用情况,如果第i位为1,说明排列中已经使用了数字i+1。
状态定义
dp[mask][i]表示数集为mask且以i+1结尾的排列数目,在这个定义下,答案就应该是Σi∈[0,n)dp[2n−1][i]。
状态转移
先初始化排列长度为1时的答案,由于任何数字放在第一位都是可以的,因此dp[2i][i]=1,i∈[0,n)。然后枚举所有状态mask,只要mask中超过一个1就进行状态转移,枚举排列的最后两个数i和j,i是最后一个数,j是倒数第二个数。
状态转移需要满足i+1|sz或sz|i+1,其中sz为mask中1的数量,|表示整除符号。状态转移方程为dp[mask][i]=Σj∈[0,n),i≠jdp[mask⊕2i][j]。
复杂度分析
时间复杂度
枚举mask的时间复杂度为O(2n),内部双重循环枚举序列的后两个元素时间复杂度为O(n2)。因此,整个算法的时间复杂度为O(n2×2n)
空间复杂度
状态数量为n2n,因此算法的额外空间复杂度为O(n2n)。
C++ 代码
const int N = 16;
int dp[1<<N][N];
class Solution {
public:
int countArrangement(int n) {
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; i++) {
dp[1<<i][i] = 1;
}
for(int mask = 1; mask < 1<<n; mask++) {
int sz = __builtin_popcount(mask);
if(sz == 1) continue;
for(int i = 0; i < n; i++) {
if(mask>>i&1) {
// i放在排列的最后
for(int j = 0; j < n; j++) {
if(i == j) continue;
if((i + 1) % sz == 0 || sz % (i + 1) == 0) {
dp[mask][i] += dp[mask^(1<<i)][j];
}
}
}
}
}
int ans = 0;
for(int i = 0; i < n; i++) {
ans += dp[(1<<n) - 1][i];
}
return ans;
}
};