多校联训 DP 专题
【UR #20】跳蚤电话
将加边变为加点,方案数为 \((n-1)!\) 除以一个数,\(dp\) 每种方案要除的数之和即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int ver[200005],ne[200005],head[100005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
const int md=998244353;
int dp[100005],siz[100005],inv[100005];
void dfs(int x,int fi){
int res=1;siz[x]=1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);siz[x]+=siz[u];
res=1ll*res*dp[u]%md;
}
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dp[x]=(dp[x]+1ll*res*siz[u]%md*inv[siz[x]]%md*inv[siz[x]-siz[u]])%md;
}
dp[x]=(dp[x]+1ll*res*inv[siz[x]])%md;
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
int ans=1;
for(int i=head[1];i;i=ne[i]){
int u=ver[i];
dfs(u,1);ans=1ll*ans*dp[u]%md;
}
for(int i=1;i<n;i++)ans=1ll*ans*i%md;
printf("%d\n",ans);
return 0;
}
【UR #12】密码锁
发现一张竞赛图的强连通分量个数等于将这张图划分成两个集合使得集合之间的边方向相同的方案数 +1 ,因此可以枚举集合的划分,这里直接做的复杂度是 \(O(2^n)\) ,发现对于一个联通块射击到的点数最多为边数 +1 ,因此可以对特殊边所涉及到的每个联通块分别枚举划分,做一下背包即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=998244353,inv2=(md+1)/2;
inline int pwr(int x,int y){
int res=1;
while(y){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
int n,m;
int x,y,z;
int aw[2005],bw[2005],cw[2005];
vector<int> vec[45];
int a[45][45],stk[45],top;
bool vis[45];
void init(int x){
if(vis[x])return;
vis[x]=1;
stk[++top]=x;
for(auto u:vec[x])init(u);
}
int f[45],g[45],h[45];
int stk0[45],top0,stk1[45],top1;
void dfs(int pos){
if(pos>top){
int res=1;
for(int i=1;i<=top0;i++){
for(int j=1;j<=top1;j++)res=1ll*res*a[stk0[i]][stk1[j]]%md;
}
res=1ll*res*bw[top0*top1]%md;
g[top0]=(g[top0]+res)%md;
return;
}
stk0[++top0]=stk[pos];
dfs(pos+1);--top0;
stk1[++top1]=stk[pos];
dfs(pos+1);--top1;
}
inline void calc(){
for(int i=0;i<=n;i++)h[i]=f[i],f[i]=0;
for(int i=0;i<=n;i++){
for(int j=0;i+j<=n;j++)f[i+j]=(f[i+j]+1ll*h[i]*g[j])%md;
}
}
long long sum=0;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)a[i][j]=5000;
}
aw[0]=1;for(int i=1;i<=n*n;i++)aw[i]=1ll*aw[i-1]*5000%md;
for(int i=0;i<=n*n;i++)bw[i]=pwr(aw[i],md-2);
cw[0]=1;for(int i=1;i<=n*n;i++)cw[i]=1ll*cw[i-1]*10000%md;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
a[x][y]=z;a[y][x]=10000-z;
vec[x].push_back(y);vec[y].push_back(x);
}
f[0]=1;
for(int i=1;i<=n;i++)if(!vis[i]){
top=0;init(i);
for(int j=0;j<=n;j++)g[j]=0;
dfs(1);calc();
}
int ans=0;
for(int i=1;i<n;i++)ans=(ans+1ll*f[i]*aw[i*(n-i)]%md*cw[n*(n-1)/2-i*(n-i)])%md;
ans=1ll*(ans+cw[n*(n-1)/2])%md*cw[n*(n-1)/2]%md;
printf("%d\n",(ans+md)%md);
return 0;
}
【UR #17】滑稽树上滑稽果
显然,无论在什么情况下,最优解都是一条链,而且每个点的滑稽度不小于所有点的 \(\text{and}\) 之和,因此可以设 \(dp[s]\) 表示拉出一条链,使得链的最下面一个点的滑稽度为 \(s\) 的最小代价,暴力枚举转移是 \(O(n\times a)\) 的,实际上转移时只需要枚举子集,判断是否有点的权值与 \(S\) 与 \(T\) 的差集无交即可,时间复杂度 \(O(3^{\log a})\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
bool vis[1<<18];
const int lim=(1<<18)-1;
inline void fwt(){
for(int i=0;i<18;i++){
for(int s=0;s<(1<<18);s++){
if((s>>i)&1)continue;
vis[s|(1<<i)]|=vis[s];
}
}
}
long long dp[1<<18],tmp=lim;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
vis[x]=1;tmp&=x;
}
fwt();
memset(dp,0x3f,sizeof(dp));
dp[lim]=n*tmp;
for(int s=lim;s;s--){
if((s&tmp)!=tmp)continue;
for(int u=(s&(s-1));1;u=(s&(u-1))){
int delta=((s-u)^lim);
if(vis[delta])dp[u]=min(dp[u],dp[s]+u-tmp);
if(!u)break;
}
}
printf("%lld\n",dp[tmp]);
return 0;
}
【UNR #2】梦中的题面
CF1342F Make It Ascending
CF1239E Turtle
可以发现最优摆放方式一定是最小值和次小值一个放左上角一个放右下角,上面升序排列,下面倒序排列。最优行走路线要么将上面一行走完,要么将下面一行走完。
背包算出将最小值和次小值去除后的所有可能,取最优结果即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[55],sum;
bitset<1250005> dp[25];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)scanf("%d",&a[i+n]);
sort(a+1,a+2*n+1);
dp[0][0]=1;
for(int i=3;i<=2*n;i++){
sum+=a[i];
for(int j=n-1;j;j--)dp[j]|=(dp[j-1]<<a[i]);
}
int ans=1e9,up=0;
for(int i=0;i<=sum;i++)if(dp[n-1][i]){
if(ans>max(a[1]+a[2]+i,a[1]+a[2]+sum-i)){
ans=max(a[1]+a[2]+i,a[1]+a[2]+sum-i);
up=i;
}
}
vector<int> UP,DOWN;
int tot=n-1;
for(int i=3;i<=2*n;i++){
if(tot&&dp[tot-1][up-a[i]]){
UP.push_back(a[i]);
up-=a[i];tot--;
}else DOWN.push_back(a[i]);
}
sort(UP.begin(),UP.end());reverse(UP.begin(),UP.end());
sort(DOWN.begin(),DOWN.end());
printf("%d ",a[1]);
for(auto it:DOWN)printf("%d ",it);puts("");
for(auto it:UP)printf("%d ",it);
printf("%d",a[2]);
return 0;
}
CF1403C Chess Rush
CF613E Puzzle Lover
将路径拆成三个部分,两边哈希,中间 \(dp\) ,需要特判长度为 \(1\) 和 \(2\) 的部分。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
char a[2][2005],s[2005];
unsigned long long le[2][2005],ri[2][2005],hshs[2005],bas[2005];
inline unsigned long long Get(unsigned long long *hsh,int l,int r){
if(l>r)return 0;
return hsh[r]-hsh[l-1]*bas[r-l+1];
}
inline unsigned long long rGet(unsigned long long *hsh,int l,int r){
if(l>r)return 0;
return hsh[l]-hsh[r+1]*bas[r-l+1];
}
inline void init(){
bas[0]=1;
for(int i=1;i<=max(n,m);i++)bas[i]=131*bas[i-1];
for(int i=1;i<=m;i++)hshs[i]=hshs[i-1]*131+s[i];
for(int i=1;i<=n;i++)le[0][i]=le[0][i-1]*131+a[0][i];
for(int i=1;i<=n;i++)le[1][i]=le[1][i-1]*131+a[1][i];
for(int i=n;i>=1;i--)ri[0][i]=ri[0][i+1]*131+a[0][i];
for(int i=n;i>=1;i--)ri[1][i]=ri[1][i+1]*131+a[1][i];
}
const int md=1e9+7;
int dp[2][2005][2005][2];
inline int calc(){
int res=0;init();
for(int i=0;i<=n;i++){
dp[0][i][0][0]=1;
for(int j=1;j<=m;j++)dp[0][i][j][0]=0;
for(int j=1;j<=m;j++)dp[0][i][j][1]=0;
dp[1][i][0][0]=1;
for(int j=1;j<=m;j++)dp[1][i][j][0]=0;
for(int j=1;j<=m;j++)dp[1][i][j][1]=0;
}
for(int i=1;i<=n;i++){
for(int j=2;j<=i&&2*j<=m;j++){
if(rGet(ri[0],i-j+1,i)*bas[j]+Get(le[1],i-j+1,i)==Get(hshs,1,2*j))dp[1][i][2*j][1]=1;
if(rGet(ri[1],i-j+1,i)*bas[j]+Get(le[0],i-j+1,i)==Get(hshs,1,2*j))dp[0][i][2*j][1]=1;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[0][i]==s[j]){
dp[0][i][j][0]=(dp[0][i][j][0]+dp[0][i-1][j-1][0])%md;
dp[0][i][j][0]=(dp[0][i][j][0]+dp[0][i-1][j-1][1])%md;
if(j!=1)dp[0][i][j][1]=(dp[0][i][j][1]+dp[1][i][j-1][0])%md;
}
if(a[1][i]==s[j]){
dp[1][i][j][0]=(dp[1][i][j][0]+dp[1][i-1][j-1][0])%md;
dp[1][i][j][0]=(dp[1][i][j][0]+dp[1][i-1][j-1][1])%md;
if(j!=1)dp[1][i][j][1]=(dp[1][i][j][1]+dp[0][i][j-1][0])%md;
}
// cout<<i<<" "<<j<<": "<<endl;
// cout<<dp[0][i][j][0]<<" "<<dp[0][i][j][1]<<endl;
// cout<<dp[1][i][j][0]<<" "<<dp[1][i][j][1]<<endl;
if(((m-j)&1)||(m-j)/2>n-i||m-j==2)continue;
if(Get(le[0],i+1,i+(m-j)/2)*bas[(m-j)/2]+rGet(ri[1],i+1,i+(m-j)/2)==Get(hshs,j+1,m)){
// cout<<"calced 0"<<endl;
res=(res+dp[0][i][j][0])%md;
res=(res+dp[0][i][j][1])%md;
}
if(Get(le[1],i+1,i+(m-j)/2)*bas[(m-j)/2]+rGet(ri[0],i+1,i+(m-j)/2)==Get(hshs,j+1,m)){
// cout<<"calced 1"<<endl;
res=(res+dp[1][i][j][0])%md;
res=(res+dp[1][i][j][1])%md;
}
}
}
// puts("---");
return res;
}
int main(){
scanf("%s",a[0]+1);n=strlen(a[0]+1);
scanf("%s",a[1]+1);
scanf("%s",s+1);m=strlen(s+1);
int ans=0;
ans=calc();
reverse(a[0]+1,a[0]+n+1);
reverse(a[1]+1,a[1]+n+1);
ans=(ans+calc())%md;
if(m==1){
for(int i=1;i<=n;i++)if(a[0][i]==s[1])ans=(ans-1+md)%md;
for(int i=1;i<=n;i++)if(a[1][i]==s[1])ans=(ans-1+md)%md;
}
if(m==2){
for(int i=1;i<=n;i++)if(a[0][i]==s[1]&&a[1][i]==s[2])ans=(ans-1+md)%md;
for(int i=1;i<=n;i++)if(a[1][i]==s[1]&&a[0][i]==s[2])ans=(ans-1+md)%md;
}
printf("%d",ans);
return 0;
}
CF1466H Finding satisfactory solutions
分析一下图的性质:
图中有若干个白色环(已确定)。然后连黑色边,使得所有的白色环形成一个 DAG ,并且一个点向 \(j\) 个点连黑色边的方案数为 \(j!(n-1-j)!\) 。
显然相同长度的白色环可以看做同一种。
设大小为 \(i\) 的白色环有 \(c_i\) 个,则不同的子图有 \(\prod(c_i+1)\) 个,这个数字不会超过 \(1440\) ,所以状态数很少。
然后考虑数 \(\text{DAG}\) 的个数,设 \(f(S)\) 为子图 \(S\) 构成 \(\text{DAG}\) 的方案数。
根据套路,枚举 \(S\) 的一个子图 \(T\) ,表示图中入度为 \(0\) 的点。将 \(T\) 连向 \(S-T\) ,此时还要满足 \(S-T\) 中的每一个点都要被连到,所以要容斥一下。
设 \(\mathrm{ways}(x,y)\) 表示 \(y\) 个点向 \(x\) 个点连边的方案数。
那么有转移 :
\(\mathrm{ways}\) 可以直接 \(O(n^2)\) 预处理,由于最多只有 \(1440\) 个状态,\(\text{DP}\) 可以直接枚举子图转移,复杂度 \(O(1440^2)\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[45];
const int md=1e9+7;
bool vis[45];
int Getsiz(int x,int siz){
if(vis[x])return siz;
vis[x]=1;return Getsiz(a[x],siz+1);
}
int c[45];
int ways[45][45],fac[45],inv[45];
inline int C(int x,int y){
if(x<y)return 0;
return 1ll*fac[x]*inv[y]%md*inv[x-y]%md;
}
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=1ll*fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
ways[0][0]=1;
for(int i=0;i<=n;i++){
for(int j=0;j<=i;j++)ways[1][i]=(ways[1][i]+1ll*C(i,j)*fac[j]%md*fac[n-1-j])%md;
for(int j=2;j<=n;j++)ways[j][i]=1ll*ways[j-1][i]*ways[1][i]%md;
}
}
int dp[2005],pre[45],nxt[45];
inline int rk(int *tmp){
int res=0;
for(int i=1;i<=n;i++)res=(c[i]+1)*res+tmp[i];
return res;
}
inline void translate(int *tmp,int x){
for(int i=n;i;i--){
tmp[i]=x%(c[i]+1);
x/=(c[i]+1);
}
}
int main(){
scanf("%d",&n);init();
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)if(!vis[i])c[Getsiz(i,0)]++;
dp[0]=1;
int lim=1;
for(int i=1;i<=n;i++)lim*=(c[i]+1);lim--;
for(int i=1;i<=lim;i++){
translate(nxt,i);
int tot0=0,tot1=0;
for(int j=1;j<=n;j++)tot0+=nxt[j],tot1+=nxt[j]*j;
for(int j=0;j<i;j++){
translate(pre,j);
int tmp0=0,tmp1=0,coe=1;
for(int j=1;j<=n;j++)tmp0+=pre[j],tmp1+=pre[j]*j;
for(int j=1;j<=n;j++)coe=1ll*coe*C(nxt[j],pre[j]);
if(!((tot0-tmp0)&1))coe*=-1;
dp[i]=(dp[i]+1ll*dp[j]*coe%md*ways[tot1-tmp1][tmp1])%md;
}
}
printf("%d\n",(dp[lim]+md)%md);
return 0;
}
CF582D Number of Binominal Coefficients
令 \(v_p(n)\)(\(p\) 是质数,\(n\) 是正整数)表示 \(n\) 的标准分解式中的 \(p\) 的幂次。
发现 \(v_p(n!)=\sum_{i=1}^{\infty}\limits\lfloor \frac{n}{p^i}\rfloor\) ,这等于将 \(n\) 写成 \(p\) 进制数之后的所有前缀之和。
而 \(v_p({n\choose k})=v_p(n!)-v_p(k!)-v_p((n-k)!)\) ,因此 \(v_p({n\choose k})\) 就等于 \(k\) 与 \(n-k\) 在 \(p\) 进制意义下相加时的进位的次数。
数位 \(dp\) 即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int md=1e9+7;
int p,alpha,a[2005],len;
char A[2005];
struct Big_based_p{
int len,x[4005];
inline void init(){
while(::len){
long long t=0;
for(int i=::len;i>=1;i--){
t=t*10+a[i],a[i]=t/p,t%=p;
if(!a[i]&&i==::len)--::len;
}
x[++len]=t;
}
}
int f[3503][3503][2][2];
inline void work(){
f[len+1][0][0][1]=1;
for(int i=len;i>=1;i--){
int c1=1ll*(p+1)*p/2%md,c2=1ll*(x[i]+1)*x[i]/2%md,c3=1ll*p*(p-1)/2%md;
int c4=1ll*x[i]*((p<<1)-x[i]-1)/2%md,c5=1ll*x[i]*(x[i]-1)/2%md,c6=1ll*x[i]*((p<<1)-x[i]+1)/2%md;
for(int j=0;j<=len-i+1;j++){
int f0=f[i+1][j][0][0],f1=f[i+1][j][0][1];
int f2=f[i+1][j][1][0],f3=f[i+1][j][1][1];
if(!f0&&!f1&&!f2&&!f3)continue;
f[i][j][0][0]=(1ll*c1*f0%md+1ll*c2*f1%md+1ll*c3*f2%md+1ll*c4*f3%md)%md;
f[i][j][0][1]=(1ll*(x[i]+1)*f1%md+1ll*(p-x[i]-1)*f3%md)%md;
f[i][j+1][1][0]=(1ll*c3*f0%md+1ll*c5*f1%md+1ll*c1*f2%md+1ll*c6*f3%md)%md;
f[i][j+1][1][1]=(1ll*x[i]*f1%md+1ll*(p-x[i])*f3%md)%md;
}
}
int res=0;
for(int i=alpha;i<=len;i++)res=(res+(f[1][i][0][0]+f[1][i][0][1])%md)%md;
printf("%d\n",res);
}
}lim;
int main(){
scanf("%d%d%s",&p,&alpha,A+1);len=strlen(A+1);
if(alpha>4000)return puts("0"),0;
for(int i=1;i<=len;i++)a[i]=A[len-i+1]-'0';
lim.init();lim.work();
return 0;
}
[AGC034E] Complete Compress
考虑枚举最终聚集的点,即枚举根节点,将不同子树中的棋子的深度两两抵消,判断是否能将所有棋子移到根。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[2005];
int ver[4005],ne[4005],head[2005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
int siz[2005],dis[2005],f[2005];
void dfs(int x,int fi){
siz[x]=a[x];int son=0;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);siz[x]+=siz[u];
dis[x]+=dis[u]+siz[u];dis[u]=dis[u]+siz[u];
if(dis[u]>dis[son])son=u;
}
if(!son)f[x]=0;
else if(2*dis[son]<=dis[x])f[x]=dis[x]/2;
else f[x]=dis[x]-dis[son]+min(f[son],(2*dis[son]-dis[x])/2);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%1d",&a[i]);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
int res=1e9;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)siz[j]=0;
for(int j=1;j<=n;j++)dis[j]=0;
for(int j=1;j<=n;j++)f[j]=0;
dfs(i,i);
if(dis[i]&1)continue;
if(2*f[i]>=dis[i])res=min(res,dis[i]/2);
}
if(res==1e9)res=-1;
printf("%d\n",res);
return 0;
}
[AGC020E] Encoding Subsets
枚举最后一段是否压缩,如何压缩,状态数不高于平方级别,可以记忆化搜索。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
const int md=998244353;
map<__int128,int> dp[105];
int dfs(__int128 x,int len){
if(dp[len].count(x))return dp[len][x];
int res=dfs(x>>1,len-1)*(x&1?2:1)%md;
int len0=1;
for(__int128 lim=2;len0<=len;lim<<=1,len0++){
__int128 tmp=x%lim;
int len1=len0;
for(__int128 j=lim;len1+len0<=len;j*=lim,len1+=len0){
tmp&=(x/j);
res=(res+1ll*dfs(x/j/lim,len-len0-len1)*dfs(tmp,len0))%md;
}
}
return dp[len][x]=res;
}
char s[105];
int main(){
scanf("%s",s);
n=strlen(s);__int128 tmp=0;dp[0][0]=dp[1][0]=1;dp[1][1]=2;
for(int i=0;i<n;i++)tmp=tmp*2+s[i]-'0';
printf("%d\n",dfs(tmp,n));
return 0;
}
[AGC036D] Negative Cycle
图中不存在负环等价于差分约束系统有解,由于初始的 \(n-1\) 条边无法删去,因此最终的权值一定是单调不升的,差分后变为一个长度为 \(n-1\) 的 \(01\) 序列。
对于 \(i\to j\ (i<j)\) 的边,当 \(i,j\) 之间没有 \(1\) 时需要被删去,\(i\to j\ (i>j)\) 的边,在 \(i,j\) 之间有至少两个 \(1\) 时需要被删去,\(dp\) 即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
long long A[505][505],B[505][505],f[505][505],ans=1e18;
inline long long val(int k,int j,int i){
return B[j+1][i]+A[n][j]-A[n][k]-A[i][j]+A[i][k];
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j)continue;
scanf("%lld",&A[i][j]);
}
}
for(int j=1;j<=n;j++){
for(int i=j;i;i--)B[i][j]=B[i+1][j]+B[i][j-1]-B[i+1][j-1]+A[i][j];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)A[i][j]=A[i][j]+A[i-1][j]+A[i][j-1]-A[i-1][j-1];
}
for(int i=1;i<n;i++){
f[i][0]=val(0,0,i);ans=min(ans,f[i][0]+val(0,i,n));
for(int j=1;j<i;j++){
f[i][j]=1e18;
for(int k=0;k<j;k++)f[i][j]=min(f[i][j],f[j][k]+val(k,j,i));
ans=min(ans,f[i][j]+val(j,i,n));
}
}
printf("%lld\n",ans);
return 0;
}
[AGC028D] Chords
发现这个圆上的问题其实可以直接铺平到序列上,考虑在每个联通块的最左和最右端点处统计该连通块。
预处理 \(g[i]\) 表示 \(i\) 给点两两之间任意匹配的方案数,对于每个区间,枚举最左边的小联通块,容斥掉不合法方案数即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
const long long md=1e9+7;
int vis[605],tot[605][605];
unsigned long long hsh[605][605],tag[605];
inline unsigned long long rd(){
return 1ll*rand()*rand()*20050115+rand()*2005+rand();
}
long long dp[605][605],g[605],ans;
int main(){
scanf("%d%d",&n,&k);n<<=1;
g[0]=1;
for(int i=2;i<=n;i+=2)g[i]=g[i-2]*(i-1)%md;
for(int i=1;i<=k;i++){
int x,y;scanf("%d%d",&x,&y);
unsigned long long tmp=rd();
tag[x]^=tmp;tag[y]^=tmp;vis[x]=-1;vis[y]=-1;
}
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
hsh[i][j]=hsh[i][j-1]^tag[j];
tot[i][j]=tot[i][j-1]+1+vis[j];
}
}
for(int len=1;len<=n;len++){
for(int l=1;l+len<=n;l++){
int r=l+len;
if(hsh[l][r])continue;
dp[l][r]=g[tot[l][r]];
for(int t=l;t<r;t++)dp[l][r]=(dp[l][r]-dp[l][t]*g[tot[t+1][r]])%md;
ans=(ans+dp[l][r]*g[tot[1][n]-tot[l][r]])%md;
}
}
printf("%lld",(ans+md)%md);
return 0;
}
[AGC022F] Checkers
首先考虑连边,建出一棵树,把每次消掉的 \(B\) 的父亲设成 \(A\) ,那么就会发现每一个点的贡献就是 \(a_ic_ix_i\) 其中 \(a_i=2^d\),\(d\) 为深度,\(c_i\) 是 \(1\) 或 \(-1\)。
因为 \(x_i\) 很大,所以任意两个点之间的贡献可以看做不会互相抵消,那么也就是说对于两个方案只要存在一个 \(i\) 使得 \(c_i\) 或者 \(a_i\) 不同,那么就算做不同的方案。
然后发现 \(a_i\) 只跟深度有关,那么这里先讨论 \(c_i\),我们可以默认根节点的 \(c_i=1\),那么发现对于一个点,它的 \(c_i\) 取决于它儿子个数的奇偶性和父亲选这个儿子的时候,之前选了儿子个数的奇偶性。
然后我们考虑根据这个性质进行dp,设 \(f_{i,j}\) 表示选了 \(i\) 个点,有 \(j\) 个点的儿子个数是奇数
那么每次就可以枚举一个当前层数有 \(k\) 个节点,然后我们发现是偶数的儿子个数为 \(\frac {k-j}{2}\) (如果发现 \(k-j\) 的奇偶性不对那就不转移),也就是说如果不计算下一层的影响的话,将会有 \(\frac {k-j}{2}\) 个点与父亲的符号相同。
考虑计算下一层的影响,枚举一个 \(x\) 表示实际上有 \(x\) 个节点和父亲不同,那么至少需要 \(|\frac {k-j}{2}-x|\) 个奇数点。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=1e9+7;
int n,dp[55][55];
long long fac[55],inv[55];
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=inv[i]*inv[i-1]%md;
}
long long C(int n,int m){
return fac[n]*inv[m]%md*inv[n-m]%md;
}
int main(){
scanf("%d",&n);init();
dp[1][1]=dp[1][0]=n;
for(int i=1;i<n;i++){
for(int j=1;j<=n-i;j++){
for(int k=0;k<=j;k++){
if((j-k)&1)continue;
int x=(j-k)/2;
for(int d=0;d<=j;d++){
dp[i+j][abs(x-d)]=(dp[i+j][abs(x-d)]+dp[i][k]*C(n-i,j)%md*C(j,d))%md;
}
}
}
}
printf("%d\n",dp[n][0]);
return 0;
}
CF379G New Year Cactus
仿照树上背包的形式,在仙人掌上做背包即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dp[3][2505][2505],tmp[2505],siz[2505],tot[2][3][2505],ANS[3][2505];
inline void calc(int x,vector<int> vec){
if(vec.size()==1){
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[1][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[2][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[0][x][i]=tmp[i];
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[1][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[1][x][i]+dp[1][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[1][x][i]=tmp[i];
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[2][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[2][x][i]+dp[2][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[2][x][i]=tmp[i];siz[x]+=siz[vec[0]];
}
else {
int u=vec.back();vec.pop_back();
reverse(vec.begin(),vec.end());vec.push_back(x);
{
int sum=siz[u];
for(auto it:vec)sum+=siz[it];
for(int i=0;i<=sum;i++)ANS[0][i]=-1e9;
for(int i=0;i<=sum;i++)ANS[1][i]=-1e9;
for(int i=0;i<=sum;i++)ANS[2][i]=-1e9;
}
for(int t=0;t<3;t++){
int *f[3]={tot[0][0],tot[0][1],tot[0][2]},*g[3]={tot[1][0],tot[1][1],tot[1][2]};
int sum=siz[u];for(int i=0;i<=sum;i++)f[t][i]=dp[t][u][i];
for(int i=0;i<=sum;i++)f[(t+1)%3][i]=-1e9;
for(int i=0;i<=sum;i++)f[(t+2)%3][i]=-1e9;
for(auto it:vec){
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[0][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[1][i]+dp[0][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[2][i]+dp[0][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[0][i]=tmp[i];
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[1][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[1][i]+dp[1][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[1][i]=tmp[i];
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[2][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[2][i]+dp[2][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[2][i]=tmp[i];sum+=siz[it];
swap(f[0],g[0]);swap(f[1],g[1]);swap(f[2],g[2]);
}
for(int i=0;i<=sum;i++)ANS[0][i]=max(ANS[0][i],f[0][i]);
if(t!=2)for(int i=0;i<=sum;i++)ANS[1][i]=max(ANS[1][i],f[1][i]);
if(t!=1)for(int i=0;i<=sum;i++)ANS[2][i]=max(ANS[2][i],f[2][i]);
}
int sum=siz[u];for(auto it:vec)sum+=siz[it];siz[x]=sum;
for(int i=0;i<=sum;i++)dp[0][x][i]=ANS[0][i];
for(int i=0;i<=sum;i++)dp[1][x][i]=ANS[1][i];
for(int i=0;i<=sum;i++)dp[2][x][i]=ANS[2][i];
}
}
vector<int> vec[2505];
int dfn[2505],low[2505],cnt;
int stk[2505],top;
void tarjan(int x){
siz[x]=1;dp[0][x][1]=-1e9;
dp[1][x][0]=1;dp[1][x][1]=-1e9;dp[2][x][0]=-1e9;
dfn[x]=low[x]=++cnt;stk[++top]=x;
for(auto u:vec[x]){
if(!dfn[u]){
tarjan(u);low[x]=min(low[x],low[u]);
if(low[u]>=dfn[x]){
vector<int> tmp;
while(stk[top]!=u)tmp.push_back(stk[top--]);
tmp.push_back(stk[top--]);calc(x,tmp);
}
}
else low[x]=min(low[x],dfn[u]);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
vec[x].push_back(y);vec[y].push_back(x);
}
tarjan(1);
for(int i=0;i<=n;i++)printf("%d ",max(dp[0][1][i],max(dp[1][1][i],dp[2][1][i])));
return 0;
}
[USACO22FEB] Phone Numbers P
考虑将后三位的值以及某一位是否可以作为转移点记录在状态中,复杂度比较高,发现很多状态是没用的,写一个结构体,用 \(\text{map}\) 记录状态即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int T;
int n,cnt[1<<10];
char s[100005];
const int md=1e9+7;
bool vis2[1<<10],vis4[1<<10];
inline void init(){
vis2[(1<<1)+(1<<2)]=vis2[(1<<2)+(1<<3)]=vis2[(1<<4)+(1<<5)]=vis2[(1<<5)+(1<<6)]=vis2[(1<<7)+(1<<8)]=vis2[(1<<8)+(1<<9)]=1;
vis2[(1<<1)+(1<<4)]=vis2[(1<<4)+(1<<7)]=vis2[(1<<2)+(1<<5)]=vis2[(1<<5)+(1<<8)]=vis2[(1<<3)+(1<<6)]=vis2[(1<<6)+(1<<9)]=1;
vis4[(1<<1)+(1<<2)+(1<<4)+(1<<5)]=vis4[(1<<2)+(1<<3)+(1<<5)+(1<<6)]=1;
vis4[(1<<4)+(1<<5)+(1<<7)+(1<<8)]=vis4[(1<<5)+(1<<6)+(1<<8)+(1<<9)]=1;
for(int i=0;i<(1<<10);i++)cnt[i]=cnt[i>>1]+(i&1);
}
inline int Get2(int i){
return (1<<s[i])|(1<<s[i-1]);
}
inline int Get4(int i){
return (1<<s[i])|(1<<s[i-1])|(1<<s[i-2])|(1<<s[i-3]);
}
struct node{
int a0,a1,a2,a3;
node(int _a0,int _a1,int _a2,int _a3){
a0=_a0;a1=_a1;a2=_a2;a3=_a3;
}
inline bool operator <(const node &b)const{
if(a0!=b.a0)return a0<b.a0;
if(a1!=b.a1)return a1<b.a1;
if(a2!=b.a2)return a2<b.a2;
return a3<b.a3;
}
};
inline bool check(int s,int N,int S){
if(cnt[s]!=N)return 0;
if((s&S)!=s)return 0;
if(!vis4[S])return 0;
return 1;
}
map<node,int> f,g;
inline void solve(){
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)s[i]-='0';
f.clear();g.clear();f[node(-1,-1,-1,0)]=1;
for(int i=1;i<=n;i++){
for(auto it:f){
node pre=it.first;
for(int t=1;t<=9;t++){
node T(pre.a1|(1<<t),pre.a2|(1<<t),pre.a3|(1<<t),-1);
if(pre.a0!=-1&&(pre.a0|(1<<t))==Get4(i)&&vis4[Get4(i)])T.a3=0;
if(T.a1!=-1&&T.a1==Get2(i)&&vis2[Get2(i)])T.a3=0;
if(T.a2!=-1&&T.a2==(1<<s[i]))T.a3=0;
if(T.a0!=-1&&(i==n||!check(T.a0,3,Get4(i+1))))T.a0=-1;
if(T.a1!=-1&&(i>=n-1||!check(T.a1,2,Get4(i+2))))T.a1=-1;
if((~T.a0)||(~T.a1)||(~T.a2)||(~T.a3))g[T]=(g[T]+it.second)%md;
}
}
swap(g,f);g.clear();
}
int ans=0;
for(auto it:f)if(!it.first.a3)ans=(ans+it.second)%md;
printf("%d\n",ans);
}
int main(){
init();
scanf("%d",&T);
while(T--)solve();
return 0;
}
[ARC068D] Solitaire
首先发现那个双端队列中的数一定先单调递减,然后再单调递增,最小值为 \(\text{1}\)。
现在考虑从双端队列中取数,那么当我们取到 \(\text{1}\) 这个数时,我们会在原来的双端队列中取到类似这样的两个数列:(分别用红、蓝表示)
那么红、蓝两数列的总长度为 \(k\),剩下的就是绿色的一段,长度为 \(n-k\),我们每次可以从两端任意取,共取 \(n-k-1\) 次,方案数为 \(2^{n-k-1}\) 。
所以我们只用考虑前 \(k\) 个数的取法,然后乘上 \(2^{n-k-1}\) 就好了。
由图像,我们看一下弹出序列需要满足什么条件:
首先第 \(k\) 位肯定是 \(\text{1}\)。(题目要求)
前 \(k\) 个数,一定是由一个或两个单调递减的数列混合而成的(这两个数列就是图中的红、蓝两个数列),并且其中的一个单调递减的数列肯定包含 \(\text{1}\) 这个点(蓝色数列)。
后 \(n-k\) 个数,其最大值应小于某一个提到的单调数列的最小值。也就是绿色段的最大值小于红色段的最小值。
那我们不妨设 \(f(i,j)\) 表示已经取了前 \(i\) 个数,他们的最小值为 \(j\) 时的方案数。(满足上面的 \(\text{3}\) 个条件)
那么我们设选的这 \(i\) 个数分成的两个单调递减的序列分别为 \(A\)、\(B\),其中最小值 \(j\) 在 \(A\) 中,\(B\) 中的最小值为 \(\text{minn}\)。
设除了我们选的这 \(i\) 个数剩下的 \(n-i\) 个数组成的集合为 \(S\),\(S\) 中最大的数为 \(\text{maxS}\)。
由 \(f(i,j)\) 的定义得,\(B\) 序列已经满足了第 \(\text{3}\) 个条件,即 \(\text{maxS}<\text{minn}\)。
我们现在要从 \(S\) 中选一个数,加入 \(A\) 或 \(B\) 里面。
考虑构造:
假设加入的数 \(x\) 满足 \(x<j\),那么我们就把它加到 \(A\) 的末尾,此时仍满足所有条件。所以 \(f(i,j)\) 向 \(f(i+1,1)\sim f(i+1,j-1)\) 转移。
假设加入的数 \(x\) 满足 \(x\geq j\),如果加入 \(A\) 中,\(A\) 就不满足单调递减的性质了,所以只能加入 \(B\) 中。
然后发现只能把 \(S\) 中的最大值 \(\text{maxS}\) 加入到 \(B\) 的队尾(而且必须得满足 \(\text{maxS}\geq j\),不然会在第一种情况中算重)。
由于 \(\text{maxS}\) 是 \(S\) 中的最大的数,所以当 \(\text{maxS}\) 加入 \(B\) 数列成为 \(B\) 数列的最小值后,仍大于 \(S\) 中的所有数,满足条件。
所以 \(f(i,j)\) 向 \(f(i+1,j)\) 转移。
至于为什么 \(S\) 中除 \(\text{maxS}\) 的某一个数 \(x\) 都不能加入 \(B\) 的队尾:因为加入后,\(B\) 中的最小值就变成了 \(x\),而 \(S\) 中的最大值仍为 \(\text{maxS}\)。此时 \(x<\text{maxS}\),不满足条件。
所以通过 \(f(i,j)\) 就可以向 \(f(i+1,1\sim j)\) 转移了。
显然答案就是 \(2^{n-k-1}\times\sum\limits_{i=2}^{n-k+2}f(k-1,i)\) 。
发现这等价于一个格路计数问题,可以用类似卡特兰数的方式做到线性。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
const int md=1e9+7;
int fac[4005],inv[4005];
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n+k;i++)fac[i]=1ll*fac[i-1]*i%md;
for(int i=2;i<=n+k;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n+k;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
}
inline long long C(int x,int y){
if(!y)return 1;
if(x<0||y<0||x<y)return 0;
return 1ll*fac[x]*inv[y]%md*inv[x-y]%md;
}
inline long long pwr(int x,int y){
int res=1;
while(y>0){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
int main(){
scanf("%d%d",&n,&k);init();
int tmp=1ll*((C(n+k-3,n-2)-C(n+k-3,n))%md+md)%md*pwr(2,n-k-1)%md;
printf("%d\n",n==1&&k==1?1:tmp);
return 0;
}