[SMOJ2090]数谜

97 篇文章 0 订阅
15 篇文章 0 订阅

20%:
数字最多有 5 位,可以枚举全排列,判断是否为 m 的倍数。但要注意去重,可以直接数组计数。
时间复杂度:记 n 的位数为 w ,则总共有 Pww 种可能,即 O(w!)

40%:(考试的时候貌似全班都这样写的)
可以用 dfs,从前到后考虑每位填什么数字,这样还可以避免出现重复。
时间复杂度上限: O(10w) ,不过实际上是根本达不到的,因为随着搜索到后面,剩下可选的数字越少。
参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

long long n, m;
int len, ans, cnt[11];

void dfs(long long k, int l) {
    if (l == len) {
        ans += !(k % m);
//      if (!(k % m)) printf("%d\n", k);
        return;
    }
    if (l && cnt[0]) {
        --cnt[0];
        dfs(k * 10LL, l + 1);
        ++cnt[0];
    }
    for (int i = 1; i < 10; i++)
        if (cnt[i]) {
            --cnt[i];
            dfs(k * 10LL + i, l + 1);
            ++cnt[i];
        }
}

int main(void) {
    freopen("2090.in", "r", stdin);
    freopen("2090.out", "w", stdout);
    scanf("%lld%lld", &n, &m);
//  printf("%I64d %d\n", n, m);
    for (; n; n /= 10) ++cnt[n % 10], ++len;
    dfs(0LL, 0);
    printf("%d\n", ans);
    return 0;
}


100:DP。
因为题目限定最终一定是原来的数字,只不过排列不同,我都写出 dfs 了,还没有认识到能“一个一个选”,潜意识更是直接排除掉了“选/不选”这种暗示。
事实上,如果没有 m 的倍数这个限制,可以用组合数学算出答案,也可以用 DP 解决。如果是后者,则会对我们得到正解有帮助。
f(i,S) 为:在最终数字中的前 i 位,由原数的 S 集合( 218 )组成。那么就有

f(i,S)=xSf(i1,Sx)

需要注意的是重复的数字和前导 0 需要特判一下,前者可以通过限定相同数字必须按顺序取,后者则在边界条件作规定即可。
这样,时间复杂度为 O(w2×2w)

现在的问题中多了一个限制,即最终的数字要是 m 的倍数,其实也不难,但需要有化归的思想。即,x m 的倍数,当且仅当 xmodm=0
同时我们知道,取模运算是满足许多优美的性质的。
假设已经知道了前 (i1) 位组成数字对 m 取模的结果,只要乘上 10,加上当前位数字,再对 m 取模,就得到了当前方案中前 i 位组成数字对 m 取模的结果。

这不禁启示我们,有限制,那就加上一维。记 f(i,S,j) 为最终数字中的前 i 位,由原数的 S 集合组成,且这个数字对 m 取模的结果为 j 的方案数,那么就有

这样,用这个三维状态的 DP,加上转移时候需要枚举的 x ,总的时间复杂度为 O(w2×2w×m),就可以完美解决本题了。注意之前所说的特殊情况仍要判断。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

//const int MAXBIT = 20;
//const int MAXM = 100 + 5;

int m;
int num[20];
long long n, f[1 << 19][101];

int bitcount(int stat) { //统计一个数的二进制表示有多少个 1
    int c;
    for (c = 0; stat; c++) stat = stat & (stat - 1);
    return c;
}

int main(void) {
    freopen("2090.in", "r", stdin);
    freopen("2090.out", "w", stdout);
    scanf("%lld%d", &n, &m);

    int cnt = 0;
    while (n) { //将原数一位位拆分出来
        num[cnt++] = n % 10;
        n /= 10;
    }
    sort(num, num + cnt); //排好序之后方便相同数字的特判

    int upper_lim = 1 << cnt;
    f[0][0] = 1;
    for (int i = 0; i < cnt; i++) //已选几位
        for (int j = 0; j < upper_lim; j++) //当前集合
            if (bitcount(j) == i) //顺推,恰好已选了 i 个数才有可能是当前集合
                for (int k = 0; k < m; k++) //当前余数
                    if (f[j][k]) //有值才住后推,否则没意义
                        for (int l = 0; l < cnt; l++) //接下来要选原数(排序后)第几位
                            if ((i || num[l]) && !(j & (1 << l)) && (!l || num[l] != num[l - 1] || (j & (1 << l - 1)))) f[j | (1 << l)][(k * 10 + num[l]) % m] += f[j][k];
                            //分别对应:首位不能为 0,要被选的数不能出现在当前集合中(即之前不能被选过,否则会重复选), 相同的数要按先后次序取
    printf("%lld\n", f[upper_lim - 1][0]);
    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值