魔法甜点之和:小包的新挑战 | 豆包MarsCode AI刷题

52 阅读5分钟

问题描述

众所周知,小包是一名非常喜欢吃甜点的小朋友,他在工作时特别爱吃下午茶里的甜食。

这天,下午茶小哥像往常一样送来了今天的 N 个甜点。小包对每个甜点有自己的喜爱值。但是今天的他不再贪心,并不想要喜爱值越高越好。他今天吃的甜点的喜爱值的和,一定要等于他的预期值 S。

但是他的预期值很高,小哥的甜点似乎不一定满足得了他,所以他准备了 M 个魔法棒,每个魔法棒可以对一个他要吃的甜点使用 1 次,使用后这个甜点的喜爱值会变成原来的喜爱值的阶乘。无法对一个甜点使用多次魔法棒,也不需要使用完所有魔法棒,也无法对不吃的甜点使用魔法棒。

小包很好奇,他有多少种方案,可以吃到喜爱值刚好为他的预期值的甜点。如果 2 种方案,食用了不同的甜点,或者对不同的甜点使用了魔法棒,都算作不同的方案。

数据范围

10%的数据保证 M = 0, 1 <= N <= 10

30%的数据保证 1 <= N <= 12

100%的数据保证 1 <= N <= 25,  0 <= M <= N, 1 <= S <= 101610^{16}

这道题乍一眼看上去很像是动态规划的题目。但经过仔细的思考发现这条路是走不通的,状态压缩并不可行,而直接按背包那更是天方夜谭。

N很小但是又不是那么小,这完全不给状态压缩活路,但是也在暗示我们可以思考另外一种解法,那就是搜索。

单纯的搜索当然是不可行的,枚举一个甜点吃或不吃,用或者不用魔法棒,那么将是3种情况,那么理论上最大的时间复杂度是O(3n)O(3^n)

这很显然已经超过了我们的限制。那么如何做呢。 OI界有不少搜索的优化方案。当我们在计数的时候,发现计数的原理满足乘法定律,那么就可以往折半搜索这里去靠近了。

n小但是不完全小,所以我们可以想办法让它更小。 我们不妨将这个n均等分成两部分,然后先分别在这两部分上搜索,看看能组合出一些什么样子的喜爱值。因为这相当于直接搜索了,所以能把每一种情况都模拟出来。

对于当前的喜爱值,我们只关心预期值-当前值是否能够被构造出来。 所以我们先搜索前n/2的部分,将所有的喜爱值组合用哈希表记录下来。

再搜索后面n/2的部分时,我们每搜索到一个结果,立刻在原有的哈希表中查询预期值-搜索结果是否存在,存在的话那么就相当于我们又多了一种方案。当然记得,魔法棒的使用次数不能超过m,所以我们还需要记录魔法棒使用了的次数,以防多算。

最后我们来思考一下剪枝,和进一步优化这个算法。 剪枝并没有太多出路。

观察数据范围,我们发现当喜爱值比较大的时候,它的阶乘必然超过最后的限制,所以可以直接不搜索阶乘那一步。

同样的,当加上这个喜爱值的阶乘以后超过了限制的话同样是不可行的,所以我们也应该把它舍去。

进一步优化这个算法,我们可以将折半写成折多几半,但是这样会增加编码的复杂度,况且对于这道题而言折半已经是足够优秀的算法了,因此没有必要折磨自己。

而且最后在统计的时候,多段仍然要一步步合并,难免出现些问题。

最后附上一段C++代码,ACM模式不是核心代码模式, 问就是我是OIer习惯了也懒得改而且写的还有些问题。 比如忘记考虑魔法棒使用次数小于m。忘了判断了。

不过这不重要。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

template <class T>
inline void read(T &x)
{
    int fg = 1; char ch = getchar(); x = 0;
    for(;ch < '0' || ch > '9';ch = getchar()) fg = ch == '-' ? -1 : 1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) x = x * 10 + (ch ^ '0'); x *= fg;
}

#define ll long long

const int P = 70921;
const int N = 1e5 + 5;

int f(int x){ return (x % P + P) % P; }

struct hash_map
{ 
    int last[N],cnt = 0,stk[N],tot = 0;
    struct node{ int nxt,key,tot; } a[N];
    void insert(int x)
    {
        int fg = 1;
        for(int p = last[f(x)];p;p = a[p].nxt)
            if(a[p].key == x) { ++a[p].tot, fg = 0; break; }
        if(fg) a[++cnt] = (node){ last[f(x)],x,1 }, last[f(x)] = cnt, stk[++tot] = f(x);
    }
    ll get(int x)
    {
        for(int p = last[f(x)];p;p = a[p].nxt)
            if(a[p].key == x) return 1ll * a[p].tot;
        return 0ll;
    }
} hash[2];

const int M = 30;

int n,m,Max;
ll S,a[M],s[M],ans = 0;

void preparation()
{
    s[1] = 1, Max = 1; 
    for(int i = 2;i <= 19; ++ i)
        s[i] = s[i - 1] * i, Max = s[i] <= S ? i : Max;
}

void dfs(int x,int lim,int fg,ll sum,int t)
{
    if(sum > S || t > m) return;
    if(x > lim)
        { hash[fg].insert(sum); return; }
    
    dfs(x + 1,lim,fg,sum,t),
    dfs(x + 1,lim,fg,sum + a[x],t);
    if(a[x] <= Max) dfs(x + 1,lim,fg,sum + s[a[x]],t + 1);
    return;
}

int main()
{
    freopen("P23.in","r",stdin); 
    freopen("P23.out","w",stdout);

    read(n), read(m), read(S);
    for(int i = 1;i <= n; ++ i) read(a[i]);

    preparation();

    dfs(1,n / 2,0,0,0), dfs(n / 2 + 1,n,1,0,0);

    for(int i = 1;i <= hash[0].tot; ++ i)
        for(int p = hash[0].last[hash[0].stk[i]];p;p = hash[0].a[p].nxt)
            if(hash[0].a[p].key <= S) ans += 1ll * hash[0].a[p].tot * hash[1].get(S - hash[0].a[p].key);

    printf("%lld\n",ans);

    fclose(stdin); fclose(stdout);
    return 0;
}