AtCoder Regular Contest 096
C.Everything on It
题目描述
解法
先思考一个简化的问题,如果要求是 \(1,2...n\) 都在其中至少出现 \(1\) 次我们会怎么做?直接上容斥,我们枚举出现次数 \(=0\) 数的个数,然后其他的乱选即可。
上述方法是可扩展的,我们可以枚举出现次数 \(\leq 1\) 数的个数,那么可以写出式子:
其中 \(f(i)\) 表示钦定 \(i\) 个数出现次数 \(\leq 1\) 的方案数,关键的 \(\tt observation\) 是涉及到的集合数量不会超过 \(i\) 个,而且知道了集合数量方案数是会好算很多的。所以再处理一个 \(g(i,j)\) 表示用 \(j\) 个集合去覆盖 \(i\) 个数,每个数最多被覆盖一次的方案数,转移:
转移的含义是考虑当前的数 新增一个集合 \(/\) 填入以前的集合(\(j\) 种方案)\(/\) 不覆盖,那么 \(f(i)\) 就很容易表示了:
解释一下后面的 \(2^{j(n-j)}\),实际上就是把未钦定的元素考虑进去,它们加入这些集合是任意的。
只能说这道题完全在我的能力范围以内,时间复杂度 \(O(n^2)\)
#include <cstdio>
const int M = 3005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,g[M][M],C[M][M];
void add(int &x,int y) {x=(x+y)%m;}
int qkpow(int a,int b,int p)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%p;
a=a*a%p;
b>>=1;
}
return r;
}
signed main()
{
n=read();m=read();g[0][0]=1;
for(int i=0;i<=n;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
add(C[i][j],C[i-1][j-1]+C[i-1][j]);
}
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++)
{
if(j) add(g[i][j],g[i-1][j-1]);
add(g[i][j],(j+1)*g[i-1][j]);
}
for(int i=0;i<=n;i++)
{
int z=C[n][i]*qkpow(2,qkpow(2,n-i,m-1),m)%m;
int f=0,x=qkpow(2,n-i,m);if(i&1) z=m-z;
for(int j=0,y=1;j<=i;j++,y=y*x%m)
add(f,g[i][j]*y);
add(ans,f*z);
}
printf("%lld\n",(ans+m)%m);
}
D.Sweet Alchemy
题目描述
解法
首先吐槽一下这场的题目真的清新,但是又具有一定的思维难度,大家多学学这场的出题人。
首先可以用差分将题目做一个基本的转化,设 \(b_i=c_i-c_{p_i}\),那么有 \(0\leq b_i\leq d\),所以可以将问题转化成:有 \(n\) 种物品,第 \(i\) 种物品的价值是子树 \(i\) 的大小,容量是子树 \(i\) 的 \(\sum m_v\),除了第一种物品可以选任意多次外,其它物品最多取 \(d\) 次。
现在得到的问题是价值很小,种类很小,但是容量很大的多重背包问题。貌似没有什么好的思路,我们不妨倒回初学背包问题的时候,有一种经典性价比贪心,也就是按照 价值 \(w_i\) \(/\) 重量 \(v_i\) 从大到小排序,但是是错的。
正确的做法是我们对于每个物品只保留 \(\min(n,d)\) 个来跑背包,设 \(dp[i]\) 表示选取 \(i\) 个物品用掉的最小容量,可以用二进制分组优化,时间复杂度 \(O(n^4\log n)\),对于剩下的物品,我们用性价比贪心即可。
感性理解上面的做法并不难,这里给出我自己的证明。
考虑排序后的物品性质,\(\forall i<j\),不存在 \(i\) 未选取的数量 \(\geq n\),\(j\) 选取的数量 \(\leq n\),否则可以调整。
那么我们只需要考虑所有满足 \(\forall i<j\),\(i\) 未选取的数量 \(<n\) 或者 \(j\) 选取的数量 \(<n\) 的情况即可。
发现如果某个物品 \(x\) 选取的数量 \(\geq n\),它实际上会要求 \([1,x)\) 这个前缀未选取的数量 \(<n\),那么如果我们把每个物品提出 \(n\) 个来跑背包(相当于决策所有情况),那么选取物品 \(x\) 就要求把 \([1,x)\) 这个前缀选取完,对应了我们的性价比贪心。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 55;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,d,mx,a[M],dp[M*M*M];vector<int> g[M];
struct node{int w,v;}s[M];
void dfs(int u)
{
for(int v:g[u])
{
dfs(v);
s[u].w+=s[v].w;
s[u].v+=s[v].v;
}
}
signed main()
{
n=read();m=read();d=read();
for(int i=1;i<=n;i++)
{
s[i].v=read();s[i].w=1;
if(i>1) g[read()].push_back(i);
}
dfs(1);mx=n*n*n;k=min(n,d);
memset(dp,0x3f,sizeof dp);dp[0]=0;
for(int i=1;i<=n;i++)
{
int x=k;
for(int j=0;(1<<j)<=x;j++)
{
int w=s[i].w*(1<<j),v=s[i].v*(1<<j);
for(int l=mx;l>=w;l--)
dp[l]=min(dp[l],dp[l-w]+v);
x-=(1<<j);
}
if(x)
{
int w=s[i].w*x,v=s[i].v*x;
for(int l=mx;l>=w;l--)
dp[l]=min(dp[l],dp[l-w]+v);
}
}
sort(s+1,s+1+n,[&](node x,node y)
{return x.w*y.v>x.v*y.w;});
int r=n,ans=0;while(s[r].w!=n) r--;
for(int i=0;i<=mx;i++)
{
if(dp[i]>m) continue;
int w=i,v=dp[i];
for(int j=1;j<r;j++)
{
int c=min(d-k,(m-v)/s[j].v);
w+=c*s[j].w;v+=c*s[j].v;
}
int c=(m-v)/s[r].v;
w+=c*s[r].w;v+=c*s[r].v;
ans=max(ans,w);
}
printf("%lld\n",ans);
}