取石子游戏
数据范围:
30分。直接暴力枚举剩下哪些堆。然后判断是否异或和为0、去掉的数量是否是d的倍数即可。
50分。考虑dp。f[i] [j] [k]表示到了第i堆石子,留下了j堆,此时子集异或和为k的方案数。\(n^2*max(a[i])\)递推即可。
70分。首先我们先把上面所述50分的dp优化一下,考虑真正对答案有贡献的肯定是去掉的个数为d的倍数。所以我们传递状态的时候,只需要考虑留下的堆的数量在mod d下是什么值就好。那么递推复杂度变为\(n*d*max(a[i])\)。有20分的数据,本质不同的a[i]只有不超过5个。显然可以用数组存起来所有子集异或和,递推的时候只考虑这些。复杂度是\(n*d*2^5\)。
100分。前面的一个n我们已经优化到了d。所以现在考虑从a[i]下手。但是每次两个数异或的值随机大小,我们怎么给缩小范围啊?排序!反正排序不影响答案,我们把a数组按照从大到小排序。假如当前到了a[i],而\(a[i]<2^L\),因为我们考虑的是如何让剩下的数异或和为0,那么它能影响到状态显然都小于\(2^L\)。所以只需要枚举\(0\)~\(2^L\)。
-
复杂度证明:
小结:比赛的时候,一定先打暴力。比如这道题50分的暴力很好打(为了确保正确性可以先打30的暴力来对拍)。跟着数据范围想优化暴力,思考算法的可行性之后再敲。今天上午想了线性基好像可以做,就直接去码了。码了码,调调发现算法是假的。而这时候已经11点了,如果是省选我就凉了。
Coding
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+10;
const int mod=1e9+7;
ll n,d,a[N],f[2][12][N];
ll read(){
char ch=getchar();ll num=0,f=1;
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){num=(num<<1)+(num<<3)+(ch^48);ch=getchar();}
return num*f;
}
bool cmp(ll a,ll b){return a>b;}
int main(){
n=read(),d=read();
for(int i=1;i<=n;++i) a[i]=read();
sort(a+1,a+n+1,cmp);
f[0][0][0]=1;ll last=0,now=1;
for(int i=1;i<=n;++i){
int l=log(1.0*a[i])/log(2.0)+1;
for(int j=0;j<d;++j) for(int k=pow(2.0,1.0*l);k>=0;--k) f[now][j][k]=0;
for(int j=0;j<d;++j){
for(int k=pow(2.0,1.0*l);k>=0;--k){
f[now][j][k]=(ll)(f[now][j][k]+f[last][(j-1+d)%d][k^a[i]])%mod;
f[now][j][k]=(ll)(f[now][j][k]+f[last][j][k])%mod;
}
}
int temp=now;now=last;last=temp;
}
int ans=f[last][n%d][0];
if(n%d==0) ans=(ll)(ans-1+mod)%mod;
cout<<ans<<endl;
return 0;
}
路径计数
数据范围:
这题暴力dfs没法儿写吧,反正我是写不了,机房大佬不知道怎么算的复杂度,让老师+时限想卡过去,其实根本不可能好吧。
必须朝dp的这个思路想才行。其实这种计数类的题目,又显然没有什么式子可以推,那就是dp了。
设f[s] [t] [d] [j]表示以s为起点,t为终点,走了d步到达j,中途不经过s和t的方案数。这个显然可以\(n^4*d\)转移,查询O\((1)\),复杂度O\((n^4*d+q)\),50分get。
上面所述状态因为要考虑中途不出现s和t,所以必须状态多了一维,考虑如何优化掉这一维。
-
假如我们现在要求得是从s走到t,走了d步,中途任意的方案数,显然可以\(n^3*d\)求。具体来说,设g[s] [t] [d] 表示从s走d步到t,任意走的方案数。
\[g[s][t][d]=\sum_{i=1}^{n}g[s][i][d-1]\]
- 如何求答案呢?其实是一个广义容斥,合法的方案数=所有的方案数-不合法的方案数。设f[s] [t] [d]表示从s走d到t的合法方案数。如何求不合法的方案数?想一下,一条不合法的路径,从s到t肯定有第一个不合法的点,即第一次遇到了s或者t。分两种情况:
- 第一次遇到了t,那么不合法的方案即为\(f[s][t][i]*g[t][t][d-i]\),i从1~d-1.
- 第一次遇到了s,那么不合法的方案应该是从s到s,中途不经过s和t的方案数再乘上\(g[s][t][d-i]\)。我们定义从s到s,中途不经过s和t的方案数为\(h[s][t][i]\)
- 到此,f数组的求法已经知道。现在考虑如何求h数组。其实和f数组一样,我们仍然把路径分为两种情况:
- 先遇到s,\(h[s][t][d]-\sum_{i=1}^{d-1}h[s][t][i]*g[s][s][d-i]\).
- 先遇到t,\(h[s][t][d]-\sum_{i=1}^{d-1}f[s][t][i]*g[t][s][d-i]\).
- 记得最初\(h[s][t][d]=g[s][s][d]\).
还有比较sb的一点是,手动取模,不然会T。
Coding
#include<bits/stdc++.h>
#define ll long long
#define mul(a,b) 1LL*a*b%p
using namespace std;
const int N=110;
int f[N][N][52],g[N][N][52],h[N][N][52];
int n,m,p,q,a[N][N];
inline void add(int &a,int b){a+=b;a-= a>=p? p:0;}
inline void sub(int &a,int b){a-=b;a=(a+p)%p;}
int read(){
char ch=getchar();int num=0,f=1;
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){num=(num<<1)+(num<<3)+(ch^48);ch=getchar();}
return num*f;
}
void work(){
for(int i=1;i<=n;++i) g[i][i][0]=h[i][i][0]=f[i][i][0]=1;
for(int d=2;d<=50;++d){
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
for(int k=1;k<=n;++k){
add(g[i][j][d],mul(g[i][k][d-1],g[k][j][1]));
}
}
}
}
for(int d=2;d<=50;++d){
for(int s=1;s<=n;++s){
for(int t=1;t<=n;++t){
h[s][t][d]=g[s][s][d];
for(int i=1;i<d;++i){
sub(h[s][t][d],mul(f[s][t][i],g[t][s][d-i]));
if(s==t) continue;
sub(h[s][t][d],mul(h[s][t][i],g[s][s][d-i]));
}
f[s][t][d]=g[s][t][d];
for(int i=1;i<d;++i){
sub(f[s][t][d],mul(f[s][t][i],g[t][t][d-i]));
if(s==t) continue;
sub(f[s][t][d],mul(h[s][t][i],g[s][t][d-i]));
}
}
}
}
}
int main(){
n=read(),m=read(),p=read();
for(int i=1;i<=m;++i){
int x,y;x=read(),y=read();
a[x][y]=1;
g[x][y][1]=1;
f[x][y][1]=1;
}
work();
q=read();
for(int i=1;i<=q;++i){
int x,y,z;x=read(),y=read(),z=read();
printf("%d\n",f[x][y][z]);
}
return 0;
}