密码
题意:
给出一个长度不超过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小,一般会采用计算方案数的方法,看到数据这么小!怎么也要状压才对得起这个数据,对吧?