USACO2022Jan,Feb,Open
Drought G
一道难度小于提高 T2 的简单 dp 优化题。
一个显然的性质是:对于一个确定的 \(h\) 数组,如果有解,那么按照“先操作 \((n-1,n)\) 若干次把 \(h_n\) 变成 \(0\)、再操作 \((n-2,n-1)\) 若干次把 \(h_{n-1}\) 变成 \(0\)……”一定最后可以都变成 \(0\)(当然不一定是 \(0\),这里只是举个例子)。
一个最初的想法是枚举公共高度 \(\mathfrak U\),那么把 \(H_1,H_2,...,H_n\) 都减去 \(\mathfrak U\),再进行如下的暴力 DP:
设 \(\mathfrak{F}(i,j)\) 表示前 \(i\) 个数,第 \(i\) 个数已经被 \(i+1\) 操作了 \(j\) 次的情况下,\(i\) 元组 \((h_1,h_2,...,h_i)\) 有几种合法方案。转移显然:\(\mathfrak{F}(i,j)=\sum_{k=j}^{H_i}\mathfrak{F}(i-1,k-j)\)。边界是 \(\mathfrak F(1,0)=\mathfrak F(1,1)=...=\mathfrak F(1,H_1)=1\)。复杂度 \(O(nH^2)\)。
直觉告诉我们这个公共高度的枚举十分多余,转而我们就发现对于 \(n\) 为偶数的情况,每一种有解情况我们钦定都变成 \(0\) 亦有解,因为当你把它都变成同一高度 \(\mathfrak U\) 时,将 \((1,2),(3,4),...,(n-1,n)\) 再各操作 \(\mathfrak U\) 次就都变成 \(0\) 了。这样一来,对于 \(n\) 为偶数的情况,直接做上面的 DP 并取 \(\mathfrak F(n,0)\) 即为答案。
对于奇数的情况我们似乎感到没有进一步优化空间,转而看到转移可以表示为 \(\mathfrak{F}(i,j)=\sum_{k=0}^{H_i-j}\mathfrak{F}(i-1,k)\),我们便自然地想到前缀和优化,也就是说 DP 可以做到 \(O(nH)\)。这样一来我们完全可以枚举 \(\mathfrak U\) 了。
#include <bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,mnh=mod,H[105],f[105][1005];
inline void add(int &x,int y){(x+=y)>=mod&&(x-=mod);}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&H[i]),mnh=min(mnh,H[i]);
for(int i=0;i<=H[1];i++)f[1][i]=1;for(int i=1;i<=1000;i++)add(f[1][i],f[1][i-1]);
for(int i=2;i<=n;i++){
for(int j=0;j<=H[i];j++)f[i][j]=f[i-1][H[i]-j];
for(int j=1;j<=1000;j++)add(f[i][j],f[i][j-1]);
}
if(n&1){
int sum=0;
for(int i=0;i<=mnh;i++){
memset(f,0,sizeof f);
for(int i=0;i<=H[1];i++)f[1][i]=1;for(int i=1;i<=1000;i++)add(f[1][i],f[1][i-1]);
for(int i=2;i<=n;i++){
for(int j=0;j<=H[i];j++)f[i][j]=f[i-1][H[i]-j];
for(int j=1;j<=1000;j++)add(f[i][j],f[i][j-1]);
}
add(sum,f[n][0]);
for(int j=1;j<=n;j++)H[j]--;
}
cout<<sum;
}
else cout<<f[n][0];
}
Redistributing Gifts G
最暴力的:设 \(f(s,t)\) 表示奶牛集合为 \(s\),礼物集合为 \(t\),选法方案数。每次枚举 \(\text{lowbit}\) 的奶牛配对转移,\(O(4^n)\)。
发现反正是顺次给奶牛分配礼物,所以不必大费周章用 bitmask 记录奶牛状态;并且考虑对于所有 \(2^n\) 个子集 \(\mathfrak S\) 整个算一次 \(ans_{\mathfrak S}\) 表示 G 型牛的位置状态为 \(\mathfrak S\) 时的答案(两种牛没有本质不同所以最后 \(ans_{\mathfrak S_{\tt G}}\cdot ans_{\mathfrak S_{\tt H}}\) 就是答案):设 \(f(i,t)\) 表示考虑了 \(p_1,p_2,...,p_i\) 的牛,可供选择的礼物集合为 \(t\) 的方案数;其中 \(p_1,...,p_{|\mathfrak S|}\) 表示该询问中为 G 的位置的编号。每次考虑给 \(p_i\) 分配的礼物转移。单次询问复杂度 \(O(|\mathfrak S|2^{|\mathfrak S|})\)。
对于这一部分的总复杂度分析:
考虑最开始的暴力的做法,显然,礼物集合的大小等于奶牛集合的大小才有意义,然后这种情况下,奶牛和礼物是配对出现的,对于关联性如此之强的两个集合,我们却用了二次方来存,直觉告诉我们这是冗余的。
考虑将奶牛和礼物之间的关联具体化,可通过建立有向边 \(\mathrm {cow\to gift}\) 的方式。\(n\) 点 \(n\) 边,这是一个由若干个有向环构成的图,则可以次第加点,考虑是加入上次环的开口还是闭上上次环并新开一个环起点接口。
设 \(f(s,las)\) 表示所加点集为 \(s\),最后加的点为 \(las\),有标号图形态的方案数。枚举下一个要加的点进行转移,会发现这样子,一方面,每个环都可以任意一点作为起点了,从而有多重表示方法,产生错误,另一方面,不同环之间的顺序随意了,也产生错误。这是一个让我们去重的问题,考虑钦定,钦定每个环的起点必须是环中最大点,钦定按照环中最大点递增的顺序依次考虑各个环。
这样,每次枚举下一个要加的点 \(j\),并记当前 \(s\) 的 \(\text{highbit}\) 为 \(v\)(也即当前正在考虑的环的最大点):
- \(j<v\),表示添加 \(las\to j\) 的边,转移到 \(f(s\oplus 2^j,j)\)
- \(j=v\),表示添加 \(las\to v\) 的边,将环关闭,不做转移,而将 \(ans_s\gets ans_s+f(s,las)\)
- \(j>v\),表示添加 \(las\to v\) 的边,将环关闭,并新开一环,转移到 \(f(s\oplus 2^j,j)\)
当然了,转移前还要判断所加的边是否满足题目中的配对要求。
至此,我们用 \(O(n^22^n+nq)\) 的复杂度解决了问题。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,q,rk[20][20],aa[1<<18];
ll f[1<<18][20],ans[1<<18];
char str[20];
inline int ppc(int x){int cnt=0;while(x)cnt++,x-=x&-x;return cnt;}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1,tmp;j<=n;j++){
scanf("%d",&tmp);
rk[i-1][tmp-1]=j;
}
}
for(int i=1,j=0;i<(1<<n);i<<=1,j++)aa[i]=j;
ans[0]=1;
for(int s=1;s<(1<<n);s++){
if(ppc(s)==1)f[s][aa[s]]=ans[s]=1;
int v;
for(int j=n-1;~j;j--)if(s>>j&1){v=j;break;}
for(int las=0;las<n;las++)if(s>>las&1){
for(int j=0;j<v;j++)if(!(s>>j&1)&&rk[las][j]<=rk[las][las]){
f[s^(1<<j)][j]+=f[s][las];
}
if(rk[las][v]<=rk[las][las]&&ppc(s)>1)ans[s]+=f[s][las];
for(int j=v+1;j<n;j++)if(!(s>>j&1)&&rk[las][v]<=rk[las][las]){
f[s^(1<<j)][j]+=f[s][las];
}
}
}
scanf("%d",&q);
while(q--){
scanf("%s",str);
int st1=0,st2=0;
for(int i=0;i<n;i++)if(str[i]=='H')st1|=1<<i;else st2|=1<<i;
printf("%lld\n",ans[st1]*ans[st2]);
}
}