密码
题意:
给出一个长度不超过17的数字num,然后排列这个数字所有位数上的数,使得新的数能被17整除,求这新的数中第K(<=17!)小的数。
题解:
既然要求出第K小,那么可以从高位到低位一位一位寻找,但是怎么寻找呢?可以计算出从高位到低位能被17整除的方案数,如果大了就进行下一位,小了就变大当前的这一位。
现在的问题就是求出能被17整除的方案数,数据范围很小考虑状态压缩,对于dp[S]表示已经选了所给数的状态为S的位置的数了,但是现在并没法转移因为有余数的限制那么再加一维余数好了,dp[r][S]表示已经选了所给数的状态为S的位置的数组成的新数对17取模为r的方案数。
状态转移 就是将所有状态的后继求和。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
const int mod = 17;
#define LL long long
LL dp[20][1<<18], pow[N];
int vis[20][1<<18], n;
char num[N];
LL K;
LL calc (int r, LL S, int pos) {
if (vis[r][S]) return dp[r][S];
vis[r][S] = 1;
if (S == 0) {
if (r == 0) return dp[r][S] = 1;
else return dp[r][S] = 0;
}
int begin = (pos == n) ? 1 : 0;
for (int i = begin; i <= 9; ++i) {
for (int j = 0; j < n; ++j) {
if ((1LL << j) & S && num[j] - '0' == i) {
int val = ((LL)r + (LL)i * pow[pos-1]) % mod;
if (val < 0) val += mod;
dp[r][S] += calc (val, S ^ (1 << j), pos - 1);
break;
}
}
}
return dp[r][S];
}
void print (int r, LL S, int pos, LL k) {
if (S == 0) return;
int begin = (pos == n) ? 1 : 0;
for (int i = begin; i <= 9; ++i) {
for (int j = 0; j < n; ++j) {
if ((1LL << j) & S && num[j] - '0' == i) {
int val = ((LL)r - (LL)i * pow[pos-1]) % mod;
if (val < 0) val += mod;
if (k <= dp[val][S ^ (1 << j)]) {
printf ("%d",i);
print(val, S ^ (1<<j), pos - 1, k);
return;
}
else k -= dp[val][S ^ (1 << j)];
}
}
}
}
int main () {
cin >> num >> K;
n = strlen (num);
pow[0] = 1;
for (int i = 1; i <= n; ++i) pow[i] = pow[i-1] * 10;
calc (0, (1 << n) - 1, n);
print (0, (1 << n) - 1, n, K);
return 0;
}
总结:
有点像名次树的感觉~求这种有方向性(高位到低位,名次树就是大小)的第K小,一般会采用计算方案数的方法,看到数据这么小!怎么也要状压才对得起这个数据,对吧?
浙公网安备 33010602011771号