结构上 DP 学习笔记
在非线性结构上的 DP。
I.环上 DP
环是简单非线性结构之一。常见套路诸如断环成链等,不在话下。
I.[FJOI2007]轮状病毒
论此题的一百种不同解法
首先,这题是有通项公式的——
,
或。
当然这并不是我们DP笔记的讨论内容。
可以观察到,答案相当于:
将到共个物品分成一些相邻的组,每组选出一个点,求分组方案数。(注意和可以在一起)。
我们设表示不考虑和可以在一起的方案数。
则有
我们让中后个数单独分一组,则剩下的是;这个数选出一个点,种选法。
现在我们强制和在一起;
方案数为
我们选出个节点放在两边,共有种放法;
从中选出一个连到中间,共有种选法;
剩下的部分是。
然后答案即为。
加上高精度,复杂度。
另外这个是可以通过差分达到线性递推的(当然加上高精度还是)。
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
struct Wint:vector<int>{
Wint(int n=0)
{
push_back(n);
check();
}
Wint& check(){
while(!empty()&&!back())pop_back();
if(empty())return *this;
for(int i=1; i<size(); ++i)(*this)[i]+=(*this)[i-1]/10,(*this)[i-1]%=10;
while(back()>=10)push_back(back()/10),(*this)[size()-2]%=10;
return *this;
}
}f[110],res;
Wint& operator+=(Wint &a,const Wint &b){
if(a.size()<b.size())a.resize(b.size());
for(int i=0; i!=b.size(); ++i)a[i]+=b[i];
return a.check();
}
Wint operator+(Wint a,const Wint &b){
return a+=b;
}
Wint& operator*=(Wint &a,const int &b){
for(int i=0;i<a.size();i++)a[i]*=b;
return a.check();
}
Wint operator*(Wint a,const int &b){
return a*=b;
}
void print(Wint a){
for(int i=a.size()-1;i>=0;i--)putchar(a[i]+'0');
}
int main(){
scanf("%d",&n);
f[0]=Wint(1),f[1]=Wint(1);
for(int i=2;i<=n;i++)for(int j=1;j<=i;j++)f[i]+=f[i-j]*j;
res=f[n];
for(int i=2;i<=n;i++)res+=f[n-i]*(i*(i-1));
print(res);
return 0;
}
II.CF704C Black Widow
每个点度数不超过 。建出图来就是一堆环、一堆链。
明显每个连通块独立。于是我们对于每个连通块,求出其中所有表达式的异或为 的方案数以及为 的方案数。
链的方案数随便 DP 一下即可。环的方案数随便找个位置断环成链然后 DP 即可。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,X[100100],Y[100100],cnt[2][100100],g[2],f[100100][2][2][2];
vector<int>v[100100];
bool vis[100100];
void dfsline(int x,int fa){
// printf("%d\n",x);
// for(int a=0;a<2;a++)for(int b=0;b<2;b++)for(int c=0;c<2;c++)
// printf("%d[%d,%d,%d]%d\n",x,a,b,c,f[x][a][b][c]);
vis[x]=true;
for(auto i:v[x]){
if(x==abs(Y[i]))swap(X[i],Y[i]);
int y=abs(Y[i]);
if(y==fa)continue;
for(int z=0;z<2;z++)for(int a=0;a<2;a++)for(int b=0;b<2;b++)for(int c=0;c<2;c++){
int A=a,B=b,C=c;
if(x+X[i]==0)A=!A;
if(y+Y[i]==0)B=!B;
C^=(A|B);
(f[y][z][b][C]+=f[x][z][a][c])%=mod;
}
return dfsline(y,x);
}
int G[2]={0,0};
for(int z=0;z<2;z++)for(int a=0;a<2;a++)for(int b=0;b<2;b++){
int B=b;
if(cnt[0][x])B^=a;
if(cnt[1][x])B^=!a;
G[B^0]=(1ll*f[x][z][a][b]*g[0]+G[B^0])%mod;
G[B^1]=(1ll*f[x][z][a][b]*g[1]+G[B^1])%mod;
}
g[0]=G[0],g[1]=G[1];
}
void dfscirc(int x,int fi){
vis[x]=true;
for(auto i:v[x]){
if(i==fi)continue;
if(x==abs(Y[i]))swap(X[i],Y[i]);
int y=abs(Y[i]);
if(!vis[y]){
for(int z=0;z<2;z++)for(int a=0;a<2;a++)for(int b=0;b<2;b++)for(int c=0;c<2;c++){
int A=a,B=b,C=c;
if(x+X[i]==0)A=!A;
if(y+Y[i]==0)B=!B;
C^=(A|B);
(f[y][z][b][C]+=f[x][z][a][c])%=mod;
}
return dfscirc(y,i);
}
// printf("%d %d\n",x,y);
int G[2]={0,0};
for(int z=0;z<2;z++)for(int a=0;a<2;a++)for(int b=0;b<2;b++){
// printf("%d,%d,%d:%d\n",z,a,b,f[x][z][a][b]);
int Z=a,A=z,B=b;
if(x+X[i]==0)Z=!Z;
if(y+Y[i]==0)A=!A;
B^=(A|Z);
G[B^0]=(1ll*f[x][z][a][b]*g[0]+G[B^0])%mod;
G[B^1]=(1ll*f[x][z][a][b]*g[1]+G[B^1])%mod;
}
g[0]=G[0],g[1]=G[1];
return;
}
}
int main(){
scanf("%d%d",&m,&n);
for(int i=1,k;i<=m;i++){
scanf("%d",&k);
if(k==1){scanf("%d",&k);if(k>0)cnt[0][k]++;else cnt[1][-k]++;}
else scanf("%d%d",&X[i],&Y[i]),v[abs(X[i])].push_back(i),v[abs(Y[i])].push_back(i);
}
// for(int i=1;i<=n;i++)printf("%d ",v[i].size());puts("");
g[0]=1;
for(int i=1;i<=n;i++)if(v[i].size()==1&&!vis[i]){
for(int t=0;t<2;t++){
int val=0;
if(cnt[0][i])val^=t;
if(cnt[1][i])val^=!t;
f[i][t][t][val]=1;
}
dfsline(i,0);
}
// printf("%d %d\n",g[0],g[1]);
for(int i=1;i<=n;i++)if(v[i].size()==2&&!vis[i]){
for(int t=0;t<2;t++)f[i][t][t][0]=1;
dfscirc(i,0);
}
// printf("%d %d\n",g[0],g[1]);
for(int i=1;i<=n;i++)if(!vis[i]){
int G[2]={0,0};
for(int t=0;t<2;t++){
int B=0;
if(cnt[0][i])B^=t;
if(cnt[1][i])B^=!t;
(G[B^0]+=g[0])%=mod;
(G[B^1]+=g[1])%=mod;
}
g[0]=G[0],g[1]=G[1];
}
printf("%d\n",g[1]);
return 0;
}
II.树形 DP
树形 DP 是极常见的类型。
I.[ZJOI2010]排列计数
按照这个关系可以建出一棵树出来;然后一组合法的排列就是这棵树的一组拓扑序。
设表示以为根的子树的拓扑序种数,表示以为根的子树的大小,
则有
因为这个可以看作是把所有的儿子所代表的拓扑序列归并到一起,所以直接一下找出要填的位置即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,f[1001000],sz[1001000],fac[1001000],inv[1001000];
int ksm(int x,int y){
int z=1;
for(;y;x=(1ll*x*x)%m,y>>=1)if(y&1)z=(1ll*z*x)%m;
return z;
}
int C(int x,int y){
return 1ll*fac[x]*inv[y]%m*inv[x-y]%m;
}
int main(){
scanf("%d%d",&n,&m),fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=(1ll*fac[i-1]*i)%m;
inv[n]=ksm(fac[n],m-2);
for(int i=n-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%m;
for(int i=1;i<=n;i++)f[i]=1;
for(int i=n;i>1;i--){
sz[i]++;
f[i>>1]=(1ll*f[i>>1]*C(sz[i>>1]+sz[i],sz[i])%m*f[i])%m;
sz[i>>1]+=sz[i];
}
printf("%d\n",f[1]);
return 0;
}
II.[HEOI2013]SAO
这题思路和我们之前的[ZJOI2010]排列计数](https://www.luogu.com.cn/problem/P2606)类似,也是一棵树的拓扑序数。但是,那题边只有一种情况(相当于这题的第三组的特殊限制),这题情况就比较复杂。
我们先忽略边方向的限制,把整张图看作一棵无向树。不妨令号节点为根。
发现只维护一维信息并不能准确地合并状态。此题的数据访问暗示我们采用算法,因此考虑二维DP。
设表示:在以为根的子树中,的拓扑序为的方案数。则答案为。
我们考虑将同它的某个儿子合并。设它们的当前大小分别为和。
假设我们现在要合并和。我们枚举一个,表示最终合并后,有个位于子树内的点排在了前面。
- 应该放在前面。
这时,必有,因为那个排在前面的点都必定放在前面。
则这次枚举贡献给了。
那么具体贡献了多少呢?
首先一定有和。
然后,前个位置中,有个位置是来自的,有。
后个位置中,有个位置是来自的,有。
然后最后的贡献就是这四个东西的乘积。
- 应该放在后面。
唯一有区别的是的枚举范围变成。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,f[1010][1010],head[1010],cnt,sz[1010],C[1010][1010],res,g[1010];
struct node{
int to,next,val;
}edge[2010];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val= w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=-w,head[v]=cnt++;
}
void dfs(int x,int fa){
sz[x]=1,f[x][1]=1;
for(int e=head[x],y;e!=-1;e=edge[e].next){
if((y=edge[e].to)==fa)continue;
dfs(y,x);
for(int i=1;i<=sz[x]+sz[y];i++)g[i]=0;
if(edge[e].val==-1)for(int i=1;i<=sz[x];i++)for(int j=1;j<=sz[y];j++)for(int k=j;k<=sz[y];k++)g[i+k]=(1ll*f[y][j]*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
if(edge[e].val== 1)for(int i=1;i<=sz[x];i++)for(int j=1;j<=sz[y];j++)for(int k=0;k< j;k++)g[i+k]=(1ll*f[y][j]*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
for(int i=1;i<=sz[x]+sz[y];i++)f[x][i]=g[i];
sz[x]+=sz[y];
}
}
int gt(){
char c=getchar();
while(c!='>'&&c!='<')c=getchar();
return c=='>'?1:-1;
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n),memset(head,-1,sizeof(head)),memset(f,0,sizeof(f)),cnt=0;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=1;i<n;i++){
int x,y,z;
scanf("%d",&x);
z=gt();
scanf("%d",&y);
ae(x,y,z);
}
dfs(0,-1),res=0;
for(int i=1;i<=n;i++)(res+=f[0][i])%=mod;
printf("%d\n",res);
}
return 0;
}
考虑优化。
我们看到这四个东西:
发现,只有一个是与有关的!
于是,我们可以改变枚举顺序,枚举,然后直接用的前缀和就可以了。
因为少了一重循环,复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,f[1010][1010],head[1010],cnt,sz[1010],C[1010][1010],res,g[1010],s[1010][1010];
struct node{
int to,next,val;
}edge[2010];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val= w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=-w,head[v]=cnt++;
}
void dfs(int x,int fa){
sz[x]=1,f[x][1]=1;
for(int e=head[x],y;e!=-1;e=edge[e].next){
if((y=edge[e].to)==fa)continue;
dfs(y,x);
for(int i=1;i<=sz[x]+sz[y];i++)g[i]=0;
if(edge[e].val==-1)for(int i=1;i<=sz[x];i++)for(int k=1;k<=sz[y];k++)g[i+k]=(1ll*s[y][k]*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
if(edge[e].val== 1)for(int i=1;i<=sz[x];i++)for(int k=0;k<=sz[y];k++)g[i+k]=(1ll*(s[y][sz[y]]-s[y][k]+mod)%mod*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
for(int i=1;i<=sz[x]+sz[y];i++)f[x][i]=g[i];
sz[x]+=sz[y];
}
for(int i=1;i<=sz[x];i++)s[x][i]=(s[x][i-1]+f[x][i])%mod;
}
int gt(){
char c=getchar();
while(c!='>'&&c!='<')c=getchar();
return c=='>'?1:-1;
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n),memset(head,-1,sizeof(head)),memset(f,0,sizeof(f)),cnt=0;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=1;i<n;i++){
int x,y,z;
scanf("%d",&x);
z=gt();
scanf("%d",&y);
ae(x,y,z);
}
dfs(0,-1),res=0;
for(int i=1;i<=n;i++)(res+=f[0][i])%=mod;
printf("%d\n",res);
}
return 0;
}
III.[CQOI2017]老C的键盘
和前一题 完 全 一 致。
那就不讲了,双倍经验水过。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[1010][1010],head[1010],cnt,sz[1010],C[1010][1010],res,g[1010],s[1010][1010];
struct node{
int to,next,val;
}edge[2010];
void ae(int u,int v,int w){
// printf("%d %d %d\n",u,v,w);
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
}
void dfs(int x){
sz[x]=1,f[x][1]=1;
for(int e=head[x],y;e!=-1;e=edge[e].next){
y=edge[e].to;
dfs(y);
for(int i=1;i<=sz[x]+sz[y];i++)g[i]=0;
if(edge[e].val==-1)for(int i=1;i<=sz[x];i++)for(int k=1;k<=sz[y];k++)g[i+k]=(1ll*s[y][k]*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
if(edge[e].val== 1)for(int i=1;i<=sz[x];i++)for(int k=0;k<=sz[y];k++)g[i+k]=(1ll*(s[y][sz[y]]-s[y][k]+mod)%mod*C[i+k-1][k]%mod*f[x][i]%mod*C[sz[x]+sz[y]-i-k][sz[x]-i]%mod+g[i+k])%mod;
for(int i=1;i<=sz[x]+sz[y];i++)f[x][i]=g[i];
sz[x]+=sz[y];
}
for(int i=1;i<=sz[x];i++)s[x][i]=(s[x][i-1]+f[x][i])%mod;
}
char str[1010];
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
scanf("%s",str+2);
for(int i=2;i<=n;i++)ae(i>>1,i,str[i]=='>'?1:-1);
dfs(1);
for(int i=1;i<=n;i++)(res+=f[1][i])%=mod;
printf("%d\n",res);
return 0;
}
IV.[NOI2002]贪吃的九头龙
思路1.
设表示:在以为根的子树上有个点是归大头吃的,并且第个点是归第个头吃的。
但这样做不仅复杂度高(似乎是?),还有个问题:无法保证每个头都至少吃了一个果子。
思路2.
设表示:在以为根的子树上有个点是归大头吃的,并且第个点 不是/是归大头吃的。
当时,对于一条边来说,只有一边归大头吃而另一边归小头吃时才不会有损失。证明显然。
否则,即,对于一条边来说,只有两边都归大头吃才会有损失。不归大头吃的地方,可以黑白染色直接造成没有任何地方有损失。
答案为。复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,p,head[310],cnt,f[310][310][2],g[310][2];
struct node{
int to,next,val;
}edge[610];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
void dfs(int x,int fa){
f[x][0][0]=f[x][1][1]=0;
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs(y,x);
for(int j=0;j<=p;j++)for(int u=0;u<2;u++)g[j][u]=0x3f3f3f3f;
for(int j=0;j<=p;j++)for(int k=0;k<=j;k++)for(int u=0;u<=min(j-k,1);u++)for(int v=0;v<=min(k,1);v++)g[j][u]=min(g[j][u],f[x][j-k][u]+f[y][k][v]+edge[i].val*(m==2?!(u^v):(u&v)));
for(int j=0;j<=p;j++)for(int u=0;u<2;u++)f[x][j][u]=g[j][u];
}
// printf("%d:",x);for(int i=0;i<=p;i++)printf("(%d,%d)",f[x][i][0],f[x][i][1]);puts("");
}
int main(){
scanf("%d%d%d",&n,&m,&p),memset(head,-1,sizeof(head)),memset(f,0x3f3f3f3f,sizeof(f));
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
if(m+p-1>n){puts("-1");return 0;}
dfs(1,0);
printf("%d\n",f[1][p][1]);
return 0;
}
V.CF633F The Chocolate Spree
奇奇怪怪的直径题
思路1.用多种东西拼出来直径
我们设表示:
:子树内一条路径的最大值
:子树内两条路径的最大值
:子树内一条路径,且起点为的最大值
:子树内两条路径,且有一条起点为的最大值
则答案为。
考虑如何转移。
设为的儿子集合。
则:
:
可以是子树的最大值;
可以通过子树里面一个,再加上()拼出的一条路径构成;
也可以通过构成。
至于和的选择,可以只记录,和前三大的值,也可以直接偷懒vector
排序水过。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,val[100100],head[100100],f[100100][4],cnt,res;
//0:a chain in the subtree
//1:two chains in the subtree
//2:a chain in the subtree with x is the starting point
//3:two chains in the subtree with x is one of the staring points
struct node{
int to,next;
}edge[200100];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,head[v]=cnt++;
}
void match(int x,int a,int b,int c){//use half chains from A and B to form a complete chain, and use a full chain from C.
if(a!=b&&b!=c&&c!=a)f[x][1]=max(f[x][1],f[a][2]+f[b][2]+val[x]+f[c][0]);
}
void dfs(int x,int fa){
vector<pair<int,int> >v0,v2,v3;
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs(edge[i].to,x);
f[x][0]=max(f[x][0],f[edge[i].to][0]);
f[x][1]=max(f[x][1],f[edge[i].to][1]);
f[x][2]=max(f[x][2],f[edge[i].to][2]);
f[x][3]=max(f[x][3],f[edge[i].to][3]);
v0.push_back(make_pair(f[edge[i].to][0],edge[i].to));
v2.push_back(make_pair(f[edge[i].to][2],edge[i].to));
v3.push_back(make_pair(f[edge[i].to][3],edge[i].to));
}
f[x][2]+=val[x],f[x][3]+=val[x];
sort(v0.begin(),v0.end()),reverse(v0.begin(),v0.end());
while(v0.size()<3)v0.push_back(make_pair(0,0));
sort(v2.begin(),v2.end()),reverse(v2.begin(),v2.end());
while(v2.size()<3)v2.push_back(make_pair(0,0));
sort(v3.begin(),v3.end()),reverse(v3.begin(),v3.end());
while(v3.size()<3)v3.push_back(make_pair(0,0));
f[x][0]=max(f[x][0],v2[0].first+v2[1].first+val[x]);
f[x][1]=max(f[x][1],v0[0].first+v0[1].first);
if(v0[0].second!=v2[0].second)f[x][3]=max(f[x][3],v0[0].first+v2[0].first+val[x]);
else f[x][3]=max(f[x][3],max(v0[0].first+v2[1].first,v0[1].first+v2[0].first)+val[x]);
for(int i=0;i<3;i++)for(int j=0;j<3;j++)for(int k=0;k<3;k++)match(x,v2[i].second,v2[j].second,v0[k].second);
if(v2[0].second!=v3[0].second)f[x][1]=max(f[x][1],v2[0].first+v3[0].first+val[x]);
else f[x][1]=max(f[x][1],max(v2[1].first+v3[0].first,v2[0].first+v3[1].first)+val[x]);
}
signed main(){
scanf("%lld",&n),memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)scanf("%lld",&val[i]);
for(int i=1,x,y;i<n;i++)scanf("%lld%lld",&x,&y),ae(x,y);
dfs(1,0);
printf("%lld\n",f[1][1]);
return 0;
}
思路2.二次扫描+换根
因为这两条路径一定会被一条边分成两半,两条路径各在一半里面,所以可以换根换出最大的断边。
设表示的子树中,以为起点的路径的最大值
设表示子树的直径。
设表示除了子树外的其余部分,以的父亲为起点的路径最大值
设表示除了子树外剩余部分的直径。
则答案为。
和可以一遍普通DP就能算出来;
我们设集合表示在的儿子中从大到小排序后的集合,
集合表示在的儿子中从大到小排序后的集合,
则可以从父亲边选一条,儿子边选一条;或者选两条儿子边;或者继承父亲的或儿子的。
即:
d[y]=max({d[x],max(h[x],(y==v[0]||y==v[1]?f[v[2]]:f[v[1]]))+(y==v[0]?f[v[1]]:f[v[0]])+val[x],(y==u[0]?g[u[1]]:g[u[0]])});
可以选择继承父亲的,也可以选择另一个兄弟的,即:
h[y]=max(h[x],(y==v[0]?f[v[1]]:f[v[0]]))+val[x];
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,f[100100],g[100100],h[100100],d[100100],head[100100],cnt,res,val[100100];
struct node{
int to,next;
}edge[200100];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
}
void dfs1(int x,int fa){
g[x]=val[x];
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs1(y,x);
g[x]=max(g[x],f[x]+f[y]+val[x]);
g[x]=max(g[x],g[y]);
f[x]=max(f[x],f[y]);
}
f[x]+=val[x];
}
bool cmp1(const int &x,const int &y){
return f[x]>f[y];
}
bool cmp2(const int &x,const int &y){
return g[x]>g[y];
}
void dfs2(int x,int fa){
vector<int>v,u;
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
v.push_back(edge[i].to),u.push_back(edge[i].to);
}
sort(v.begin(),v.end(),cmp1),v.push_back(0),v.push_back(0);
sort(u.begin(),u.end(),cmp2),u.push_back(0);
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
d[y]=max({d[x],max(h[x],(y==v[0]||y==v[1]?f[v[2]]:f[v[1]]))+(y==v[0]?f[v[1]]:f[v[0]])+val[x],(y==u[0]?g[u[1]]:g[u[0]])});
res=max(res,g[y]+d[y]);
// printf("(%d,%d):%d,%d,%d,%d\n",x,y,g[y],max(h[x],(y==v[0]||y==v[1]?f[v[2]]:f[v[1]])),(y==v[0]?f[v[1]]:f[v[0]]),val[x]);
h[y]=max(h[x],(y==v[0]?f[v[1]]:f[v[0]]))+val[x];
dfs2(y,x);
}
}
signed main(){
scanf("%lld",&n),memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)scanf("%lld",&val[i]);
for(int i=1,x,y;i<n;i++)scanf("%lld%lld",&x,&y),ae(x,y),ae(y,x);
dfs1(1,0),dfs2(1,0);
// for(int i=1;i<=n;i++)printf("%lld ",f[i]);puts("");
// for(int i=1;i<=n;i++)printf("%lld ",g[i]);puts("");
// for(int i=1;i<=n;i++)printf("%lld ",h[i]);puts("");
printf("%lld\n",res);
return 0;
}
VI.CF767C Garland
有两种可行方法:
-
对于一个点,它存在两个儿子,使得这两个儿子的子树中个存在一棵子树,它们的都是。
-
对于一个点,它的是,并且它的子树中存在一个子树,它的是。
然后我们只需要对于每个节点记录表示子树中是否有一个的节点即可。复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,rt,head[1001000],sum[1001000],all,val[1001000],cnt,has1[1001000];
struct node{
int to,next;
}edge[1001000];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
}
void dfs(int x){
sum[x]=val[x];
for(int i=head[x];i!=-1;i=edge[i].next){
dfs(edge[i].to),sum[x]+=sum[edge[i].to];
if(has1[edge[i].to]){
if(!has1[x])has1[x]=has1[edge[i].to];
else{printf("%d %d\n",has1[x],has1[edge[i].to]);exit(0);}
}
}
if(sum[x]==all*2&&has1[x]&&x!=rt){printf("%d %d\n",x,has1[x]);exit(0);}
if(sum[x]==all)has1[x]=x;
}
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1,x;i<=n;i++){
scanf("%d%d",&x,&val[i]),all+=val[i];
if(!x)rt=i;
else ae(x,i);
}
if(all%3!=0){puts("-1");return 0;}
all/=3;
dfs(rt);
puts("-1");
return 0;
}
VII.CF815C Karen and Supermarket
思路:一看就是树DP。
设表示:
在以为根的子树中,选了个物品,并且从到的路径上的点 没有/有 全部选上的最小花费。
则初始,,。其它全赋成。
之后背包转移即可。可以从和转移来,而只能从转移。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,f[5010][5010][2],head[5010],cnt,c[5010],d[5010],sz[5010],g[5010];//0:anything in the subtree 1:path from root must hold
struct node{
int to,next;
}edge[5010];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
}
void dfs(int x){
sz[x]=1,f[x][0][0]=0,f[x][1][0]=c[x],f[x][1][1]=c[x]-d[x];
for(int e=head[x],y;e!=-1;e=edge[e].next){
y=edge[e].to,dfs(y);
for(int i=0;i<=sz[x]+sz[y];i++)g[i]=0x3f3f3f3f3f3f3f3f;
for(int i=1;i<=sz[x];i++)for(int j=0;j<=sz[y];j++)g[i+j]=min(g[i+j],f[x][i][1]+min(f[y][j][0],f[y][j][1]));
for(int i=0;i<=sz[x]+sz[y];i++)f[x][i][1]=g[i];
for(int i=0;i<=sz[x]+sz[y];i++)g[i]=0x3f3f3f3f;
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++)g[i+j]=min(g[i+j],f[x][i][0]+f[y][j][0]);
for(int i=0;i<=sz[x]+sz[y];i++)f[x][i][0]=g[i];
sz[x]+=sz[y];
}
}
signed main(){
scanf("%lld%lld",&n,&m),memset(head,-1,sizeof(head)),memset(f,0x3f,sizeof(f));
for(int i=1,x;i<=n;i++){
scanf("%lld%lld",&c[i],&d[i]);
if(i>1)scanf("%lld",&x),ae(x,i);
}
dfs(1);
// for(int i=1;i<=n;i++)printf("%d ",sz[i]);puts("");
for(int i=1;i<=n+1;i++){
if(min(f[1][i][0],f[1][i][1])<=m)continue;
printf("%lld\n",i-1);break;
}
return 0;
}
VIII.CF1029E Tree with Small Distances
我们发现,如果一个点与连了边,那么它的儿子们以及它的父亲都会变成合法的。
因此我们可以设表示:的某个儿子中有边/自己有边/的父亲应该有边的最小值。
转移:
:可以从儿子的或转移,且儿子中至少有一个为(即,找到与差最小的那个换成)
:皆可,取即可。
:取。
复杂度。
最后说一下答案,应该是的所有儿子的的和,因为的所有儿子都相当于连了一条免费的边。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,head[1001000],cnt,f[1001000][3],res;//0:have a son;1:itself;2:have a father
struct node{
int to,next;
}edge[2001000];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
}
void dfs(int x,int fa){
int mn=0x3f3f3f3f;
f[x][1]=1;
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs(y,x);
f[x][0]+=min(f[y][0],f[y][1]),mn=min(mn,f[y][1]-f[y][0]);
f[x][1]+=min(f[y][0],min(f[y][1],f[y][2]));
f[x][2]+=min(f[y][0],f[y][1]);
if(x==1)res+=f[y][1]-1;
}
f[x][0]+=max(mn,0);
}
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),ae(x,y),ae(y,x);
dfs(1,0);
printf("%d\n",res);
return 0;
}
IX.CF1059E Split the Tree
我们假设对于每个位置,已经求出了它可以往上延伸的长度,然后考虑DP。
设表示子树被分完后的最小边的数量。再设表示当这个数量最小时,点能够往上延伸的最长长度。
这运用了贪心的思想:因为少一条边,肯定是要比无论大多少都是要更优的。再大,也只对一条边有效,中一条边和中一条边,不都是一样的吗?
我们可以很轻松地得到转移方程:
如果在上面的转移方程中,得到了,那就意味着必须在位置开新边,令,加一。
现在主要的部分就是求出了。这个可以通过倍增法在时间里预处理出来。
则总复杂度为。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,L,S,val[100100],len[100100],anc[100100][20],sum[100100],dep[100100],f[100100],g[100100];
vector<int>v[100100];
void dfs1(int x){
for(int i=1;(1<<i)<=dep[x];i++)anc[x][i]=anc[anc[x][i-1]][i-1];
for(int i=19,y=x;i>=0;i--){
if(!anc[y][i])continue;
if(sum[x]-sum[anc[y][i]]+val[anc[y][i]]>S)continue;
if(dep[x]-dep[anc[y][i]]>=L)continue;
len[x]+=(1<<i),y=anc[y][i];
}
for(auto y:v[x])anc[y][0]=x,dep[y]=dep[x]+1,sum[y]=sum[x]+val[y],dfs1(y);
}
void dfs2(int x){
for(auto y:v[x])dfs2(y),f[x]=max(f[x],f[y]),g[x]+=g[y];
f[x]--;
if(f[x]==-1)f[x]=len[x],g[x]++;
}
signed main(){
scanf("%lld%lld%lld",&n,&L,&S);
for(int i=1;i<=n;i++){
scanf("%lld",&val[i]);
if(val[i]>S){puts("-1");return 0;}
}
for(int i=2,x;i<=n;i++)scanf("%lld",&x),v[x].push_back(i);
dep[1]=1,sum[1]=val[1],dfs1(1),dfs2(1);
// for(int i=1;i<=n;i++)printf("%lld ",len[i]);puts("");
printf("%lld\n",g[1]);
return 0;
}
X.[ABC163F]path pass i
思路:
反向考虑。我们计算出不包含任何颜色为的节点的路径的数量,再用总路径数一减就行。
则,我们删去所有颜色为的节点,整棵树就会被分成许多连通块。则不经过任何一个颜色为的节点的路径数量,就是。
设表示以为根的子树中,删掉所有颜色为的点后,有多少个点与断开联系。再设表示子树大小。
则节点所在的连通块大小即为。
乍一看这状态是的。但是如果我们用std::map
维护状态,并且在合并状态时启发式合并一下,复杂度就是的。
在节点时,将所有(其中是的儿子)计入答案,它们被看作是一个连通块的根。
代码:
#include<bits/stdc++.h>
using namespace std;
#define sqr(x) 1ll*x*(x+1)>>1ll
typedef long long ll;
int n,col[200100],sz[200100];
map<int,int>f[200100];
vector<int>v[200100],res[200100];
void dfs(int x,int fa){
sz[x]=1;
for(int y:v[x]){
if(y==fa)continue;
dfs(y,x),sz[x]+=sz[y];
int cc=sz[y];
if(f[y].find(col[x])!=f[y].end())cc-=f[y][col[x]];
res[col[x]].push_back(cc);
if(f[x].size()<f[y].size())swap(f[x],f[y]);
for(auto i:f[y])f[x][i.first]+=i.second;
}
f[x][col[x]]=sz[x];
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&col[i]);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
for(int i=1;i<=n;i++)res[i].push_back(n-f[1][i]);
for(int i=1;i<=n;i++){
ll ans=sqr(n);
for(auto j:res[i])ans-=sqr(j);
printf("%lld\n",ans);
}
return 0;
}
XI.CF1312G Autocompletion
XII.UOJ#11. 【UTR #1】ydc的大树
很明显,如果我们令一个黑点为树根,设它的“好朋友”集合为,则路径中所有白节点均可以使不开心。这个可以用树上差分来进行路径加。现在关键是求出。
我们采取二次扫描与换根法。第一遍扫描,我们求出一个节点子树中所有黑点到它的距离的最大值,以及这些离它最远的黑点的。我们用一个std::pair<int,int>
来储存这两个值,记作。
我们考虑怎么求出出来——
设有一条边是。则会从f[y].first+z
最大的那个转移过来;但是,如果存在两个不同的都是最大值,显然它们的就是本身。
这是求的代码(如果不存在这个黑点,即为)。
void dfs1(int x,int fa){
if(bla[x])f[x]=make_pair(0,x);else f[x]=make_pair(-1,0);
for(auto i:v[x]){
if(i.first==fa)continue;
dfs1(i.first,x);
if(f[i.first].first!=-1)f[x].first=max(f[x].first,f[i.first].first+i.second);
}
if(f[x].first==-1)return;
int cnt=0;
for(auto i:v[x]){
if(i.first==fa)continue;
if(f[i.first].first==-1)continue;
if(f[i.first].first+i.second==f[x].first)cnt++,f[x].second=f[i.first].second;
}
if(cnt>1)f[x].second=x;
}
既然要二次扫描,我们自然要设一个,表示子树外所有节点到的最大距离及它们的。
可以从这些东西转移过来:
-
兄弟们的;
-
父亲的;
-
父亲自身(假如父亲是黑点的话)
我们把前两个东西丢进vector
中按照first
从大到小排序。选取最大值(当然不能是自身)转移即可。
当然,如果前两个东西中没有任何黑点,要考虑从父亲自身转移。
这部分的代码:
void dfs2(int x,int fa){
vector<pair<int,int> >u;
for(auto i:v[x])if(i.first!=fa&&f[i.first].first!=-1)u.push_back(make_pair(f[i.first].first+i.second,f[i.first].second));
if(g[x]!=make_pair(-1,0))u.push_back(g[x]);
sort(u.rbegin(),u.rend());
for(auto i:v[x]){
if(i.first==fa)continue;
for(auto j:u){
if(j.second==f[i.first].second)continue;
if(g[i.first]==make_pair(0,0)){g[i.first]=j;continue;}
if(g[i.first].first==j.first)g[i.first].second=x;
break;
}
if(g[i.first]==make_pair(0,0))g[i.first]=(bla[x]?make_pair(i.second,x):make_pair(-1,0));
else g[i.first].first+=i.second;
dfs2(i.first,x);
}
}
最终就是答案统计了。对于所有的黑点,如果f[x].first==g[x].first
,显然为本身,可以忽略;否则,选取f[x]
与g[x]
中较大的那个的second
,进行树上差分即可。
复杂度(瓶颈在于树上差分,求部分的那个排序其实没有必要,但是如果这样写会更加清晰)。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,anc[100100][20],dep[100100],sum[100100],mx,cnt;
vector<pair<int,int> >v[100100];
pair<int,int>f[100100],g[100100];//first:the maximum route length; second:the lca of all the 'good friends'
bool bla[100100];
void dfs1(int x,int fa){
if(bla[x])f[x]=make_pair(0,x);else f[x]=make_pair(-1,0);
for(auto i:v[x]){
if(i.first==fa)continue;
dfs1(i.first,x);
if(f[i.first].first!=-1)f[x].first=max(f[x].first,f[i.first].first+i.second);
}
if(f[x].first==-1)return;
int cnt=0;
for(auto i:v[x]){
if(i.first==fa)continue;
if(f[i.first].first==-1)continue;
if(f[i.first].first+i.second==f[x].first)cnt++,f[x].second=f[i.first].second;
}
if(cnt>1)f[x].second=x;
}
void dfs2(int x,int fa){
vector<pair<int,int> >u;
for(auto i:v[x])if(i.first!=fa&&f[i.first].first!=-1)u.push_back(make_pair(f[i.first].first+i.second,f[i.first].second));
if(g[x]!=make_pair(-1,0))u.push_back(g[x]);
sort(u.rbegin(),u.rend());
for(auto i:v[x]){
if(i.first==fa)continue;
for(auto j:u){
if(j.second==f[i.first].second)continue;
if(g[i.first]==make_pair(0,0)){g[i.first]=j;continue;}
if(g[i.first].first==j.first)g[i.first].second=x;
break;
}
if(g[i.first]==make_pair(0,0))g[i.first]=(bla[x]?make_pair(i.second,x):make_pair(-1,0));
else g[i.first].first+=i.second;
dfs2(i.first,x);
}
}
void dfs3(int x,int fa){
anc[x][0]=fa,dep[x]=dep[fa]+1;
for(auto i:v[x])if(i.first!=fa)dfs3(i.first,x);
}
void dfs4(int x,int fa){
for(auto i:v[x])if(i.first!=fa)dfs4(i.first,x),sum[x]+=sum[i.first];
}
int LCA(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=19;i>=0;i--)if(dep[x]<=dep[y]-(1<<i))y=anc[y][i];
if(x==y)return x;
for(int i=19;i>=0;i--)if(anc[x][i]!=anc[y][i])x=anc[x][i],y=anc[y][i];
return anc[x][0];
}
int main(){
scanf("%d%d",&n,&m);
for(int x;m--;)scanf("%d",&x),bla[x]=true;
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),v[x].push_back(make_pair(y,z)),v[y].push_back(make_pair(x,z));
g[1]=make_pair(-1,0);
dfs1(1,0),dfs2(1,0),dfs3(1,0);
for(int j=1;j<=19;j++)for(int i=1;i<=n;i++)anc[i][j]=anc[anc[i][j-1]][j-1];
for(int i=1;i<=n;i++){
// printf("%d:(%d,%d),(%d,%d)\n",i,f[i].first,f[i].second,g[i].first,g[i].second);
if(!bla[i]||f[i].first==g[i].first)continue;
int x,y=i;
if(f[i].first>g[i].first)x=f[i].second;
else x=g[i].second;
int lca=LCA(x,y);
sum[x]++,sum[y]++,sum[lca]--;
if(anc[lca][0])sum[anc[lca][0]]--;
}
dfs4(1,0);
for(int i=1;i<=n;i++)if(!bla[i])mx=max(mx,sum[i]);
for(int i=1;i<=n;i++)if(!bla[i])cnt+=(sum[i]==mx);
printf("%d %d",mx,cnt);
return 0;
}
XIII.CF543D Road Improvement
常规换根DP题。
我们可以设表示以为根的子树中的方案数。则有转移式
其中的意思是将边留作坏边。
显然换根DP就很好实现了。
但一个问题就是换根的时候可能会出现除数为的情形;故我们不能直接简单地除以逆元。所以我们须要预处理出来前缀积与后缀积,这样就能在转移过程中避免逆元辣。
时间复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[200100];
vector<int>v[200100],pre[200100],suf[200100];
void dfs1(int x){
f[x]=1;
for(auto y:v[x])dfs1(y),f[x]=1ll*f[x]*(f[y]+1)%mod;
if(v[x].empty())return;
pre[x].resize(v[x].size()),suf[x].resize(v[x].size());
pre[x][0]=1;for(int i=1;i<v[x].size();i++)pre[x][i]=1ll*pre[x][i-1]*(f[v[x][i-1]]+1)%mod;
suf[x].back()=1;for(int i=(int)v[x].size()-2;i>=0;i--)suf[x][i]=1ll*suf[x][i+1]*(f[v[x][i+1]]+1)%mod;
}
void dfs2(int x,int qwq=1){
for(int i=0;i<v[x].size();i++){
int y=v[x][i];
int tmp=1ll*pre[x][i]*suf[x][i]%mod*qwq%mod;
f[y]=1ll*f[y]*(tmp+1)%mod;
dfs2(y,tmp+1);
}
}
int main(){
scanf("%d",&n);
for(int i=2,x;i<=n;i++)scanf("%d",&x),v[x].push_back(i);
dfs1(1),dfs2(1);
for(int i=1;i<=n;i++)printf("%d ",f[i]);
return 0;
}
XIV.GYM102082E Eulerian Flight Tour
(原题是PDF,没有题面的直接页面,就放一个vjudge的链接罢)
首先,当是奇数时,完全图一定是欧拉图,故直接全连即可。
当是奇数时,原图是欧拉图等价于补图上每个节点的度数都为奇。每个节点度数都为奇的充分必要条件是存在一座度数全为奇的生成森林。
这里有一种复杂度不太正确的解法:
考虑首先做一遍一般图匹配。接着,在原图中长度为的路径连接着的所有点间做一般图匹配;然后是长度为的路径……
这个算法的正确性显然——假如一条路径上的所有边都被选上了,只有两端的点的奇偶性改变了;而每个节点只会出现在一条路径的一端。为了避免两条路径有交,所以我们要按照长度处理,这样两条有交的路径就会被拆成两条无交的路径。
单次一般图匹配的复杂度是(带花树算法)(别问,问就是不会,从网上弄的板子);因为所有长度的路径数量之和是的,所以总复杂度是。
虽然理论复杂度能过,但实际上常数很大,最终T掉了。
代码(带花树部分来自网络,TLE):
#include<bits/stdc++.h>
using namespace std;
void read(int &x){
x=0;
char c=getchar();
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
const int N = 110;
int T,n,m,e,cnt,tot,ans,hd[N],p[N],match[N],pre[N],vst[N],dfn[N];
queue<int>q;
struct edge{int t,nxt;}es[N*N];
inline void Add(register int u,register int v){es[++tot]=(edge){v,hd[u]};hd[u]=tot;}
inline void add(register int u,register int v){Add(u,v),Add(v,u);}
int find(register int x){return x==p[x]?x:p[x]=find(p[x]);}
inline int lca(register int u,register int v){
for(++cnt,u=find(u),v=find(v);dfn[u]!=cnt;){
dfn[u]=cnt;
u=find(pre[match[u]]);
if(v)swap(u,v);
}
return u;
}
inline void blossom(register int x,register int y,register int w){
while(find(x)!=w){
pre[x]=y,y=match[x];
if(vst[y]==2)vst[y]=1,q.push(y);
if(find(x)==x)p[x]=w;
if(find(y)==y)p[y]=w;
x=pre[y];
}
}
inline int aug(register int s){
if((ans+1)*2>n)return 0;
for(register int i=1;i<=n;++i)p[i]=i,vst[i]=pre[i]=0;
while(!q.empty())q.pop();
for(q.push(s),vst[s]=1;!q.empty();q.pop())
for(register int u(q.front()),i(hd[u]),v,w;i;i=es[i].nxt){
if(find(u)==find(v=es[i].t)||vst[v]==2)continue;
if(!vst[v]){
vst[v]=2;pre[v]=u;
if(!match[v]){
for(register int x=v,lst;x;x=lst)lst=match[pre[x]],match[x]=pre[x],match[pre[x]]=x;
return 1;
}
vst[match[v]]=1,q.push(match[v]);
}else blossom(u,v,w=lca(u,v)),blossom(v,u,w);
}
return 0;
}
bool g[N][N];
bool has[N];
bool no[N][N];
int dis[N][N],TOT;
int main(){
read(n),read(m);
for (int i=1,x,y;i<=m;++i)read(x),read(y),g[x][y]=true;
TOT=n*(n-1)/2-m;
if(n&1){
printf("%d\n",TOT);
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(!g[i][j])printf("%d %d\n",i,j);
}else{
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
if(!g[i][j])dis[i][j]=1;
else dis[i][j]=0x3f3f3f3f;
if(i==j)dis[i][j]=0;
}
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
for(int d=1;d<=n;d++){
memset(hd,0,sizeof(hd)),tot=0;
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(!has[i]&&!has[j]&&dis[i][j]==d)add(i,j);
for (int i=1;i<=n;++i) if (!match[i]) ans+=aug(i);
for (int i=1;i<=n;++i){
if(!match[i]||has[i])continue;
has[i]=true;
if(match[i]>i)continue;
int j=i;
while(j!=match[i])for(int k=1;k<=n;k++)if(!g[j][k]&&dis[match[i]][k]+1==dis[match[i]][j]){g[j][k]=g[k][j]=no[j][k]=no[k][j]=true,j=k,TOT--;break;}
}
if(ans*2==n){
for(int i=1;i<=n;i++){
bool ok=false;
for(int j=1;j<=n;j++)if(i!=j)ok|=!no[i][j];
if(!ok){puts("-1");return 0;}
}
printf("%d\n",TOT);
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(!g[i][j])printf("%d %d\n",i,j);
return 0;
}
}
puts("-1");
}
return 0;
}
下面是正解:
每个节点度数都为奇的充分必要条件还有一个,就是不存在点数为奇的连通块。假如该条件成立,我们考虑构造一组解。我们考虑对于每个连通块都求一棵生成树,然后断掉某些边使得每个节点度数都为奇。这个可以从叶子向上DP,如果一个节点的儿子中有偶数条边保留了,则其与父亲的边也需保留,否则则需断开。
需要注意的是,原图必须是连通图,等价于补图中不能有度数为的节点。假如我们发现存在这样的节点,则如果当前生成树是唯一生成树,无解;否则,考虑找到另一棵生成树。这可以通过找到一条原树中没有的边,然后强制连上它,然后重新求生成树得到。
代码:
#include<bits/stdc++.h>
using namespace std;
void read(int &x){
x=0;
char c=getchar();
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int n,m;
const int N=110;
vector<int>v[N];
int dsu[N],sz[N],deg[N];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
bool merge(int x,int y){
int p=find(x),q=find(y);
if(p==q)return true;
v[y].push_back(x),v[x].push_back(y),deg[y]++,deg[x]++,dsu[p]=q,sz[q]+=sz[p];
return false;
}
bool g[N][N];
bool no[N][N];
int TOT;
bool dfs(int x,int fa){
bool ok=true;
for(auto y:v[x]){
if(y==fa)continue;
if(dfs(y,x))no[y][x]=no[x][y]=true,ok^=1,TOT--;
}
return ok;
}
int main(){
read(n),read(m);
for (int i=1,x,y;i<=m;++i)read(x),read(y),g[x][y]=true;
TOT=n*(n-1)/2-m;
if(n&1){
printf("%d\n",TOT);
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(!g[i][j])printf("%d %d\n",i,j);
}else{
for(int i=1;i<=n;i++)dsu[i]=i,sz[i]=1;
bool ok=false;
for(int i=n;i;i--)for(int j=n;j>i;j--)if(!g[i][j])ok|=merge(i,j);
for(int i=1;i<=n;i++)if(dsu[i]==i&&(sz[i]&1)){puts("-1");return 0;}
int invalid=0;
for(int i=1;i<=n;i++)if(deg[i]==n-1){
if(!ok){puts("-1");return 0;}
invalid=i;
}
if(invalid){
for(int i=1;i<=n;i++)dsu[i]=i,sz[i]=1,deg[i]=0,v[i].clear();
int x=0,y=0;
for(int i=n;i;i--)for(int j=n;j>i;j--){
if(g[i][j])continue;
if(merge(i,j)&&!x&&!y)x=i,y=j;
}
for(int i=1;i<=n;i++)dsu[i]=i,sz[i]=1,deg[i]=0,v[i].clear();
merge(x,y);
for(int i=n;i;i--)for(int j=n;j>i;j--)if(!g[i][j])merge(i,j);
}
for(int i=1;i<=n;i++)if(dsu[i]==i)dfs(i,0);
printf("%d\n",TOT);
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(!g[i][j]&&!no[i][j])printf("%d %d\n",i,j);
}
return 0;
}
XV.[POI2013]LUK-Triumphal arch
明显题目具有可二分性。
考虑如何check。
我们发现,一个足够聪明的B,必定不会走回头路。故最终结果一定是一条从根到某个叶子的路径。
我们发现,如果一个父亲已经染掉了它所有儿子,它剩余的操作次数便可以去染儿子,以防到了某个儿子的时候完不成任务。但是儿子的操作却不能反过来贡献父亲。
所以我们可以设计出这样的DP状态:表示节点最少需要从父亲那借多少次操作才可以完成任务。设二分的值是,于是就有
因为一个父亲必须保证它所有的儿子都能在B走过去的时候防的住,所以它必须有足够的操作次数染掉所有的儿子。如果不行,就必须再找爷爷借了。一直借到根,自然根是没地方借去的,所以判断条件就是是否为。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,f[1001000],mid;
vector<int>v[1001000];
void dfs(int x,int fa){
f[x]=0;
for(auto y:v[x])if(y!=fa)dfs(y,x),f[x]+=f[y]+1;
f[x]-=mid,f[x]=max(f[x],0);
}
bool che(){
dfs(1,0);
return f[1]==0;
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
int l=0,r=n;
while(l<r){
mid=(l+r)>>1;
if(che())r=mid;
else l=mid+1;
}
printf("%d\n",r);
return 0;
}
XVI.[APIO2014]连珠线
一般的换根DP题。
明显可以看出,最终的树一定可以通过指定一个根变成一棵有根树,所有的蓝边都可以被分成两两一组,其中每组中两条边深度递增。
于是我们可以设置DP状态。表示节点,它不是/是某对蓝边的中间节点时,子树中最大的蓝边权和。
简单使用multiset
维护从哪个儿子转移过来最优即可。
然后换个根即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int inf=0xc0c0c0c0;
int n,f[200100][2],head[200100],cnt,res;
struct node{
int to,next,val;
}edge[400100];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
multiset<int>s[200100];
void dfs1(int x,int fa){
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs1(y,x);
int tmp=max(f[y][0],f[y][1]+edge[i].val);
f[x][0]+=tmp;
f[x][1]+=tmp,s[x].insert(f[y][0]+edge[i].val-tmp);
}
if(s[x].empty())f[x][1]=inf;
else f[x][1]+=*s[x].rbegin();
// printf("%d:%d %d\n",x,f[x][0],f[x][1]);
}
void dfs2(int x,int fa){
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
int tmp=max(f[y][0],f[y][1]+edge[i].val);
int fx0=f[x][0],fx1=f[x][1];
fx0-=tmp;
fx1-=tmp;
fx1-=*s[x].rbegin();
int pmt=f[y][0]+edge[i].val-tmp;
s[x].erase(s[x].find(pmt));
fx1=(s[x].empty()?inf:fx1+*s[x].rbegin());
s[x].insert(pmt);
int qwq=max(fx0,fx1+edge[i].val);
f[y][0]+=qwq;
f[y][1]=(s[y].empty()?0:f[y][1]-*s[y].rbegin());
f[y][1]+=qwq;
s[y].insert(fx0+edge[i].val-qwq);
f[y][1]+=*s[y].rbegin();
dfs2(y,x);
}
}
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
dfs1(1,0),dfs2(1,0);
for(int i=1;i<=n;i++)res=max(res,f[i][0]);
printf("%d\n",res);
return 0;
}
XVII.[CSACADEMY]Root Change
常规换根DP。设 表示 子树中以 为起点的最长路径长度,设 表示 子树中边的数量,再设 表示 子树的答案。
则 和 显然很好转移。考虑 ,则有
于是直接上 multiset
暴力换根即可。时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,f[100100],g[100100],sz[100100];
vector<int>v[100100];
multiset<pair<int,int> >s[100100];
multiset<int>t[100100];
void dfs1(int x,int fa){
for(auto y:v[x])if(y!=fa)dfs1(y,x),f[x]=max(f[x],f[y]+1),sz[x]+=sz[y]+1,s[x].insert(make_pair(f[y]+1,g[y]-(sz[y]+1))),t[x].insert(f[y]+1);
g[x]=sz[x];
if(s[x].size()==1||s[x].size()>=2&&s[x].rbegin()->first!=(++s[x].rbegin())->first)g[x]+=s[x].rbegin()->second;
}
void dfs2(int x,int fa){
for(auto y:v[x]){
if(y==fa)continue;
int fx=0,szx=sz[x]-sz[y]-1;
t[x].erase(t[x].find(f[y]+1));
if(!t[x].empty())fx=*t[x].rbegin();
t[x].insert(f[y]+1);
int gx=szx;
s[x].erase(s[x].find(make_pair(f[y]+1,g[y]-(sz[y]+1))));
if(s[x].size()==1||s[x].size()>=2&&s[x].rbegin()->first!=(++s[x].rbegin())->first)gx+=s[x].rbegin()->second;
s[x].insert(make_pair(f[y]+1,g[y]-(sz[y]+1)));
t[y].insert(fx+1);
s[y].insert(make_pair(fx+1,gx-(szx+1)));
sz[y]+=szx+1;
f[y]=*t[y].rbegin();
g[y]=sz[y];
if(s[y].size()==1||s[y].size()>=2&&s[y].rbegin()->first!=(++s[y].rbegin())->first)g[y]+=s[y].rbegin()->second;
dfs2(y,x);
}
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs1(1,0),dfs2(1,0);
for(int i=1;i<=n;i++)printf("%d\n",g[i]);
return 0;
}
XVIII.[POI2014]MRO-Ant colony
根据下取整除法的性质(),我们可以反向考虑,即从特殊边开始,计算出从每个叶子到特殊边的路径上,要除以的那个分母是什么。
这个可以直接一遍dfs就出来了(可以把它当成DP)。注意,当一段路径的分母已经爆时就可以直接退出了,因为这样子不会有蚂蚁到得了特殊边。
然后,对于一个分母,所有的蚁群数量都是合法的;故我们直接对蚁群数量排序然后二分再差分即可。
时间复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int LIM=1e9;
int n,m,k,sp[1001000],U,V;
ll dif[1001000],res;
vector<int>v[1001000],u;
void dfs(int x,int fa,int lam){
if(v[x].size()==1){u.push_back(lam);return;}
if(1ll*lam*(v[x].size()-1)>LIM)return;
lam*=(v[x].size()-1);
for(auto y:v[x])if(y!=fa)dfs(y,x,lam);
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)scanf("%d",&sp[i]);
scanf("%d%d",&U,&V),v[U].push_back(V),v[V].push_back(U);
for(int i=1,x,y;i+1<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(U,V,1),dfs(V,U,1);
sort(sp+1,sp+m+1);
for(auto i:u){
ll l=1ll*k*i,r=1ll*(k+1)*i;
if(l>LIM)continue;
dif[lower_bound(sp+1,sp+m+1,l)-sp]++;
dif[lower_bound(sp+1,sp+m+1,r)-sp]--;
}
for(int i=1;i<=m;i++)dif[i]+=dif[i-1],res+=dif[i];
printf("%lld\n",res*k);
return 0;
}
XIX.[POI2015]MOD
比较恶心的题目。
首先,有一个结论,即如果把两棵树通过某种方式连接起来,新树的直径的端点一定来自于原本两棵树的直径端点集合。
则考虑新树的最大直径,明显就是把两棵树的直径直接连一块,就是两棵树的直径之和再加一。
考虑新树的最小直径,则应该选择两树直径的中点(如果直径长度为奇数则随便选一个)连一块,这样新树的直径就是 。当然,还得与两条直径本身取一个。
于是我们就用换根DP求出表示子树内的直径,求出其子树外的直径(这里换根DP我一开始用的是multiset
维护,但是会莫名其妙MLE
。故最后不得不全面换成vector
才不出问题)。然后两个拼一块就能找出所有新树的直径的最大值/最小值。
代码(非常丑陋):
#include<bits/stdc++.h>
using namespace std;
int n,f[500100],g[500100],h[500100],FA[500100],mx,mn=0x3f3f3f3f;
//f[i]:the maximal length starting from i; g[i]: the maximal length in i's subtree; h[i]:the maximal length outside that.
vector<int>v[500100],s[500100],t[500100];
void dfs1(int x,int fa){
for(auto y:v[x]){
if(y==fa)continue;
FA[y]=x;
dfs1(y,x);
f[x]=max(f[x],f[y]+1);
s[x].push_back(f[y]+1);
t[x].push_back(g[y]);
g[x]=max(g[x],g[y]);
}
sort(s[x].rbegin(),s[x].rend());while(s[x].size()>3)s[x].pop_back();
sort(t[x].rbegin(),t[x].rend());while(t[x].size()>2)t[x].pop_back();
if(s[x].size()>=2)g[x]=max(g[x],s[x][0]+s[x][1]);
else if(s[x].size()>=1)g[x]=max(g[x],s[x][0]);
}
void dfs2(int x,int fa){
int alls=0;
for(auto i:s[x])alls+=i;
// printf("%dS",x);for(auto i:s[x])printf("%d ",i);puts("");
// printf("%dT",x);for(auto i:t[x])printf("%d ",i);puts("");
for(auto y:v[x]){
if(y==fa)continue;
if(f[y]+1>=s[x].back())h[y]=alls-(f[y]+1);
else if(s[x].size()<=2)h[y]=alls;
else h[y]=s[x][0]+s[x][1];
if(t[x][0]!=g[y])h[y]=max(h[y],t[x][0]);
else if(t[x].size()>=2)h[y]=max(h[y],t[x][1]);
t[y].push_back(h[y]);
sort(t[y].rbegin(),t[y].rend());while(t[y].size()>2)t[y].pop_back();
if(s[x][0]!=f[y]+1)s[y].push_back(s[x][0]+1);
else if(s[x].size()>=2)s[y].push_back(s[x][1]+1);
else s[y].push_back(1);
sort(s[y].rbegin(),s[y].rend());while(s[y].size()>3)s[y].pop_back();
dfs2(y,x);
}
}
int S,dp,inva,nd,rt;
void dfs3(int x,int fa,int dep){
if(dep>dp)S=x,dp=dep;
for(auto y:v[x])if(y!=fa&&y!=inva)dfs3(y,x,dep+1);
}
bool dfs4(int x,int fa){
if(x==S){
nd--;
if(nd==0)rt=x;
return true;
}
for(auto y:v[x]){
if(y==fa)continue;
if(!dfs4(y,x))continue;
nd--;
if(nd==0)rt=x;
return true;
}
return false;
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs1(1,0),dfs2(1,0);
// for(int i=1;i<=n;i++)printf("%d:%d %d %d\n",i,f[i],g[i],h[i]);
for(int i=2;i<=n;i++)mx=max(mx,g[i]+h[i]+1),mn=min(mn,max({(g[i]+1)/2+(h[i]+1)/2+1,g[i],h[i]}));
for(int i=2;i<=n;i++){
if(mn!=max({(g[i]+1)/2+(h[i]+1)/2+1,g[i],h[i]}))continue;
printf("%d %d %d ",mn,i,FA[i]);
inva=FA[i];
S=0,dp=-1;
dfs3(i,FA[i],0);
int T=S;
S=0,dp=-1;
dfs3(T,0,0);
nd=(g[i]+2)/2;
dfs4(T,0);
printf("%d ",rt);
inva=i;
S=0,dp=-1;
dfs3(FA[i],i,0);
T=S;
S=0,dp=-1;
dfs3(T,0,0);
nd=(h[i]+2)/2,dfs4(T,0);
printf("%d\n",rt);
break;
}
for(int i=2;i<=n;i++){
if(mx!=g[i]+h[i]+1)continue;
inva=0;
printf("%d %d %d ",mx,i,FA[i]);
S=0,dp=-1;
dfs3(i,FA[i],0);
printf("%d ",S);
S=0,dp=-1;
dfs3(FA[i],i,0);
printf("%d\n",S);
break;
}
return 0;
}
XX.[JLOI2016/SHOI2016]侦察守卫
神题。
见代码即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,p,a[500100],f[500100][25],g[500100][25],res=0x3f3f3f3f;
//f[i,j]:minimum cost when there're at most j layers left uncovered
//g[i,j]:minimum cost when there're at least j layers outside the subtree covered
bool sp[500100];
vector<int>v[500100];
void dfs(int x,int fa){
if(sp[x])f[x][0]=g[x][0]=a[x];//at special points, empty state still need a guard; at normal points, empty state doesn't need a guard.
for(int i=1;i<=m;i++)g[x][i]=a[x];//at whatever points, a state which can spread outside the subtree need a guard.
g[x][m+1]=0x3f3f3f3f;//avoiding transferring outside bounds
for(auto y:v[x]){
if(y==fa)continue;
dfs(y,x);
for(int i=m;i>=0;i--)g[x][i]=min(g[x][i]+f[y][i],f[x][i+1]+g[y][i+1]);
//in this case transferring order doesn't matter.
//but g's transferring must take place before f since it needs f in transferring.
//case 1:guards in x spread into y, where there are i layers downwards y is covered from x.
//case 2:guards in y spread into x, where there are i+1 layers downward x is covered from y, and y can also cover upper levels.
for(int i=m;i>=0;i--)g[x][i]=min(g[x][i],g[x][i+1]);
//in this case it is getting a suffix minimum, where transferring order matters.
f[x][0]=g[x][0];//in fact f[x][0] and g[x][0] have the same meaning(which is a full subtree covered and nothing more)
for(int i=1;i<=m;i++)f[x][i]+=f[y][i-1];
//if there are at most i layers downwards, there should be at most i-1 layers downwards.
for(int i=1;i<=m;i++)f[x][i]=min(f[x][i],f[x][i-1]);
//getting a prefix minimum.
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
scanf("%d",&p);
for(int i=1,x;i<=p;i++)scanf("%d",&x),sp[x]=true;
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
printf("%d\n",f[1][0]);
return 0;
}
XXI.[COCI2014-2015#1] Kamp
一看题面,突然感觉很弱智,不就是求出以每个点为根到其它所有特殊点的距离之和吗?这不是随随便便换个根就完事了吗?
然后兴冲冲敲出来,一测样例全挂。
后来发现并不是这样的,因为车上可以同时搭载多人,且车最后可以就停在某个地方不回去了。
稍微想想可以发现,最终停着的位置,一定是离起点最远的特殊点;故我们直接使用 multiset
维护一下就可以换根了。求出每个节点离其最远的特殊点的距离后,以它为根的答案就是。
然后,因为车上可以搭载多人,所以实际上上述最短距离就是二倍以当前点和所有特殊点构成的虚树大小。这个可以直接通过求虚树大小的做法(按照dfs序排序再求出两两相邻点间距离)或者干脆直接再来一发换根解决。
时间复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,head[500100],cnt,g[500100];
ll f[500100],h[500100];
bool sp[500100];
struct node{
int to,next,val;
}edge[1001000];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
multiset<ll>s[500100];
void dfs1(int x,int fa){
if(sp[x])g[x]++,h[x]=0,s[x].insert(0);
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
dfs1(y,x),f[x]+=f[y]+1ll*!!g[y]*edge[i].val,g[x]+=g[y];
h[x]=max(h[x],h[y]+edge[i].val),s[x].insert(h[y]+edge[i].val);
}
}
void dfs2(int x,int fa){
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa)continue;
ll fx=f[x]-f[y]-1ll*!!g[y]*edge[i].val;
int gx=g[x]-g[y];
f[y]+=fx+1ll*!!gx*edge[i].val;
g[y]+=gx;
s[x].erase(s[x].find(h[y]+edge[i].val));
if(!s[x].empty())s[y].insert(*s[x].rbegin()+edge[i].val);
s[x].insert(h[y]+edge[i].val);
h[y]=*s[y].rbegin();
dfs2(y,x);
}
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),memset(h,0xc0,sizeof(h));
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
for(int i=1,x;i<=m;i++)scanf("%d",&x),sp[x]=true;
dfs1(1,0),dfs2(1,0);
// for(int i=1;i<=n;i++)printf("%lld %d %lld\n",f[i],g[i],h[i]);
for(int i=1;i<=n;i++)printf("%lld\n",2*f[i]-h[i]);
return 0;
}
XXII.[GYM102331J]Jiry Matchings
首先,不难想到一个 的树上背包:设 表示在以 为根的子树内,其中 没有被匹配/被匹配了,且整个子树中共匹配了 条边的最优方案。考虑优化。
我们知道一个结论——对于任意带权网络流图,随着总流量的增加,最大费用是凸函数,即差分不增的函数。这可以被这样证明:我们在 点流量的残量网络上多找到一条增广路,显然这条路一定与之前所有增广路相容。则其长度一定不大于 时的任一增广路,不然完全可以把那条短的增广路替换掉。
显然,最大带权匹配也是带权网络流模型,也因此 这两个函数理论上都是凸的。但实际上,因为 强制 被匹配上,故不一定最优;但我们如果修改 的定义为 可以被匹配,也可以不被匹配,则此时显然两个函数都是凸的。
我们考虑合并一条边连接着的两个子树(也即一对父子),不妨设父亲为 ,儿子为 ,则 。其中, 符号为我们接下来定义的一种卷积,其可以作用于数组与数组间、数组与整数间; 为连接 的边的权值,对两个函数取 意为对每一位分别取 。
下面我们来考虑如何定义卷积。明显,数组间的卷积,不妨设为 ,则有
这个卷积对于普通的 函数是只有 的计算方法的;但是,因为我们上文中出现的所有函数,按照我们之前的证明,都是凸的!我们再来看这个卷积,发现其本质是对于 在笛卡尔平面上形成的凸包的闵可夫斯基和,因为 均凸,所以是有 的计算方法的!不考虑闵可夫斯基和的性质,我们也能得到,因为差分不增,所以若 从某一个 转移而来,则 一定是在 的基础上,加上 , 二者中较大的一个得到的。贪心地处理,即可做到 。
现在考虑数组与整数间卷积,明显就直接背包即可,也可 直接得到。
于是我们合并一条边连接的子树的复杂度就是 的,而非常规树上背包的 ,但是这样还是改变不了我们树上背包 的事实。
下面考虑祭出一些奇奇怪怪的合并方式。考虑对这棵树重链剖分。
现在,我们考虑先求出每个点仅考虑其所有轻儿子的子树时,其此时的 。
明显,对于点 来说,仅考虑其一个轻儿子 时,其转移上来的东西是 。
现在要求出其所有轻儿子转移上来的东西的卷积。如果直接按顺序一个一个乘的话,复杂度是 的,因为每合并一个轻儿子, 的函数大小就会增加,而我们卷积的复杂度是与 有关的,所用这样并不能保证复杂度。
但是,如果我们对其分治地进行合并的话,复杂度就是 的,因为每个轻儿子中所有元素都会被访问恰好 次。
显然,每个轻儿子仅会被转移一次,所以处理这个轻儿子的复杂度就是 的。因为关于树剖有一个结论,所有轻儿子的子树大小之和是 的,故此部分总复杂度是 的。
下面我们考虑重链部分的转移。显然,如果仍考虑归并地转移的话——显然,这里归并的状态就需要四个数组,即区间顶有/无被匹配、区间底有/无被匹配,且当归并到长度为 的区间时需要特殊考虑,因为此时区间的顶与底相同——复杂度仍为 ,因为此时链上所有节点所代表的子树两两无交,故单次重链合并的复杂度就是 的,其中 是链顶节点。而每个链顶节点也同时是某个东西的轻儿子,故此部分复杂度仍是 的。
总复杂度 。实际实现时可以用 vector
模拟每个函数。不必担心空间、时间等常数问题,因为我写的非常随便的代码都没卡常过去了,事实上大概也跑不满。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=0x8080808080808080;
int n,head[200100],cnt;
struct node{int to,next,val;}edge[400100];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
struct Array:vector<ll>{
Array(){resize(1);}
friend Array operator +(const Array &u,const Array &v){
Array w;
w.resize(u.size()+v.size()-1);
for(int i=1,j=1,k=1;k<w.size();k++){
if(i==u.size()){w[k]=w[k-1]+v[j]-v[j-1],j++;continue;}
if(j==v.size()){w[k]=w[k-1]+u[i]-u[i-1],i++;continue;}
if(u[i]-u[i-1]>v[j]-v[j-1])w[k]=w[k-1]+u[i]-u[i-1],i++;
else w[k]=w[k-1]+v[j]-v[j-1],j++;
}
return w;
}
friend Array operator +(const Array &u,const int &v){
Array w=u;w.push_back(inf);
for(int i=1;i<w.size();i++)w[i]=max(w[i],u[i-1]+v);
return w;
}
friend Array operator |(const Array &u,const Array &v){
Array w;w.assign(max(u.size(),v.size()),inf);
for(int i=0;i<w.size();i++){
if(i<u.size())w[i]=max(w[i],u[i]);
if(i<v.size())w[i]=max(w[i],v[i]);
}
return w;
}
void print()const{for(auto i:*this)printf("%d ",i);puts("");}
};
struct Paira{
Array f,g;//f is the array for self matched-or-not, while g is the self not matched
Paira(){f=Array(),g=Array();}
Paira(Array F,Array G){f=F,g=G;}
friend Paira operator +(const Paira &u,const Paira &v){
Paira w;
w.f=(u.f+v.g)|(u.g+v.f);
w.g=u.g+v.g;
w.f=w.f|w.g;
// puts("U");u.print();
// puts("V");v.print();
// puts("W");w.print();
return w;
}
void print()const{printf("[1]"),f.print();printf("[0]"),g.print();}
}a[200100];
struct Quada{
Paira f,g;//f for upper matched-or-not, g for upper not matched
Quada(){f=Paira(),g=Paira();}
Quada(Paira F,Paira G){f=F,g=G;}
friend Quada merge(const Quada &u,const int &v,const Quada &w,bool U,bool W){
Quada r;
r.f.f=(u.f.f+w.f.f)|((u.f.g+w.g.f)+v);
r.f.g=(u.f.f+w.f.g);
if(W)r.f.g=r.f.g|((u.f.g+w.g.g)+v);
r.g.f=(u.g.f+w.f.f);
if(U)r.g.f=r.g.f|((u.g.g+w.g.f)+v);
r.g.g=(u.g.f+w.f.g);
if(U&&W)r.g.g=r.g.g|((u.g.g+w.g.g)+v);
r.f.g=r.f.g|r.g.g,r.g.f=r.g.f|r.g.g;
r.f.f=r.f.f|r.f.g|r.g.f;
// puts("U"),u.print();
// puts("W"),w.print();
// puts("R"),r.print();puts("");
return r;
}
void print()const{puts("[[F]]"),f.print();puts("[[G]]"),g.print();}
};
int fa[200100],son[201000],top[200100],sz[200100],val[200100];
void dfs1(int x){
sz[x]=1;
for(int i=head[x],y;i!=-1;i=edge[i].next){
if((y=edge[i].to)==fa[x])continue;
fa[y]=x,val[y]=edge[i].val,dfs1(y),sz[x]+=sz[y];
if(sz[y]>sz[son[x]])son[x]=y;
}
}
vector<int>v;
Paira lightsonsolve(int l,int r){
if(l==r-1)return Paira(a[v[l]].g+val[v[l]],a[v[l]].f);
int mid=(l+r)>>1;
return lightsonsolve(l,mid)+lightsonsolve(mid,r);
}
Quada heavysonsolve(int l,int r){
if(l==r)return Quada(Paira(a[v[l]].f,a[v[l]].g),Paira(a[v[l]].g,a[v[l]].g));
int mid=(l+r)>>1;
// printf("(%d,%d]:(%d,%d]+(%d,%d]\n",l,r,l,mid,mid,r);
return merge(heavysonsolve(l,mid),val[v[mid+1]],heavysonsolve(mid+1,r),l!=mid,mid+1!=r);
}
void dfs2(int x){
if(!top[x])top[x]=x;
if(son[x])top[son[x]]=top[x],dfs2(son[x]);
for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa[x]&&edge[i].to!=son[x])dfs2(edge[i].to);
v.clear();for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa[x]&&edge[i].to!=son[x])v.push_back(edge[i].to);
// printf("LIT%d:",x);for(auto i:v)printf("%d ",i);puts("");
if(!v.empty())a[x]=lightsonsolve(0,v.size());
// a[x].print();puts("");
if(top[x]!=x)return;
v.clear();for(int i=x;i;i=son[i])v.push_back(i);
// printf("CHA%d:",x);for(auto i:v)printf("%d ",i);puts("");
Quada tmp=heavysonsolve(0,v.size()-1);
a[x]=Paira(tmp.f.f,tmp.g.f);
}
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
dfs1(1),dfs2(1);
// for(int i=1;i<=n;i++)printf("%d %d %d %d %d\n",fa[i],son[i],top[i],sz[i],val[i]);
// for(int i=1;i<=n;i++)printf("%d:%d\n",i,a[i].f.size());
for(int i=1;i<n;i++)if(i<a[1].f.size())printf("%lld ",a[1].f[i]);else printf("? ");puts("");
return 0;
}
XXIII.[ABC207F]Tree Patrolling
弱智DP题,设 表示在点 ,子树中有 个点被覆盖,且 点自身状态是未被覆盖/被自身覆盖/被某个儿子覆盖,然后树上背包更新就行了。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[2010][2010][3],sz[2010],g[2010][3];//0/1/2:the node is guarded by nothing/itself/some of its sons
vector<int>v[2010];
void dfs(int x,int fa){
f[x][0][0]=1;
f[x][1][1]=1;
sz[x]=1;
for(auto y:v[x])if(y!=fa){
dfs(y,x);
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++){
(g[i+j][0]+=1ll*f[x][i][0]*f[y][j][0]%mod)%=mod;
(g[i+j+1][2]+=1ll*f[x][i][0]*f[y][j][1]%mod)%=mod;
(g[i+j][0]+=1ll*f[x][i][0]*f[y][j][2]%mod)%=mod;
(g[i+j+1][1]+=1ll*f[x][i][1]*f[y][j][0]%mod)%=mod;
(g[i+j][1]+=1ll*f[x][i][1]*f[y][j][1]%mod)%=mod;
(g[i+j][1]+=1ll*f[x][i][1]*f[y][j][2]%mod)%=mod;
(g[i+j][2]+=1ll*f[x][i][2]*f[y][j][0]%mod)%=mod;
(g[i+j][2]+=1ll*f[x][i][2]*f[y][j][1]%mod)%=mod;
(g[i+j][2]+=1ll*f[x][i][2]*f[y][j][2]%mod)%=mod;
}
sz[x]+=sz[y];
for(int i=0;i<=sz[x];i++)for(int j=0;j<3;j++)f[x][i][j]=g[i][j],g[i][j]=0;
}
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
for(int i=0;i<=n;i++){
int res=0;
for(int j=0;j<3;j++)(res+=f[1][i][j])%=mod;
printf("%d\n",res);
}
return 0;
}
XXIV.[JSOI2018]潜入行动
弱智DP题,设 表示点 ,子树内放了 个监视器,且当前节点未被监视/被儿子监视,当前节点未放监视器/放了监视器,然后手动DP一下就行了。
时间复杂度 ——虽然我自己没搞明白为什么不是 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,sz[100100],f[100100][110][2][2],g[110][2][2];//monitored by nothing/son; 0:self not put 1:self put
vector<int>v[100100];
void dfs(int x,int fa){
f[x][0][0][0]=f[x][1][0][1]=1,sz[x]=1;
for(auto y:v[x])if(y!=fa){
dfs(y,x);
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y]&&i+j<=m;j++){
(g[i+j][0][0]+=1ll*f[x][i][0][0]*f[y][j][1][0]%mod)%=mod;
(g[i+j][0][1]+=1ll*f[x][i][0][1]*(f[y][j][0][0]+f[y][j][1][0])%mod)%=mod;
(g[i+j][1][0]+=(1ll*f[x][i][1][0]*(f[y][j][1][0]+f[y][j][1][1])+1ll*f[x][i][0][0]*f[y][j][1][1])%mod)%=mod;
(g[i+j][1][1]+=(1ll*f[x][i][1][1]*(0ll+f[y][j][0][0]+f[y][j][0][1]+f[y][j][1][0]+f[y][j][1][1])+1ll*f[x][i][0][1]*(f[y][j][0][1]+f[y][j][1][1]))%mod)%=mod;
}
sz[x]+=sz[y],sz[x]=min(sz[x],m);
for(int i=0;i<=sz[x];i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)f[x][i][j][k]=g[i][j][k],g[i][j][k]=0;
}
// for(int i=0;i<=m;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)printf("%d:%d,%d,%d:%d\n",x,i,j,k,f[x][i][j][k]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
printf("%d\n",(f[1][m][1][0]+f[1][m][1][1])%mod);
return 0;
}
XXV.[Atcoder-Typical DP contest-N]木
弱智DP题,直接设 表示 子树内染色的方案数,然后每次合并一个点与它的儿子即可(具体而言,因为儿子间独立,所以方案数就是二项式系数)。
需要注意的是因为第一条边可以在任意位置,所以要以每个点为根各DP一次。但是这样每条边会被算两次,所以乘以 的逆元即可。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int inv2=5e8+4;
int n,f[1010],sz[1010],fac[1010],inv[1010],res;
vector<int>v[1010];
void dfs(int x,int fa){
f[x]=1,sz[x]=0;
for(auto y:v[x]){
if(y==fa)continue;
dfs(y,x);
f[x]=1ll*f[x]*f[y]%mod*inv[sz[x]]%mod*inv[sz[y]]%mod*fac[sz[x]+sz[y]]%mod;
sz[x]+=sz[y];
}
sz[x]++;
}
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int main(){
scanf("%d",&n);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(int i=1;i<=n;i++)dfs(i,0),(res+=f[i])%=mod;
printf("%d\n",1ll*res*inv2%mod);
return 0;
}
XXVI.[2019国家集训队]递增树列
找不到原题,这里给出简要题意:
给定一棵根为 的树,求所有树上节点构成的排列中,满足相邻两节点的 LCA 深度不降的排列数,对 取模。
数据范围:。
Observation 1.LCA 序列中的所有点必然在自根开始的一条路径中。
这很显然,如果有两个无祖先关系的点在 LCA 序列中,则它们的 LCA 必然会出现在 LCA 序列中且在该两个点之间,导致深度不可能递增。
于是就可以 DP。设 表示 的子树中有一条长度为 的序列的方案数。
考虑转移。显然该序列中前面一段的 LCA 都为 ,后一段的 LCA 都在 的某个子树中。
那么枚举这个分界点以及该子树。设该子树为 ,其中长度为 ,则后半部分答案显然为 。
考虑前半部分。问题是将一堆点排成一列且相邻的点不能来自同一子树的方案数。很明显是经典老题,可以简单处理。
问题是这部分点中可能也有来自 子树中的点。而这些点的数量显然为 ,那么就也一样插回去就行了,唯一的区别是这些点不能填到最后一个位置。最后处理即可。
考虑复杂度分析。
我们需要记录当前一共插入了多少点。因此求一遍前半部分的问题就需要 。对于 相同的 ,有关其的前半部分 DP 可以一遍 DP 求出,因此对于每个 都只需要 。
对于每条边都要求一次,因此需要 。
为了避免再记录一维表示当前子树中插入多少点,我们需要强制插入的点是子树中前一些点(这样现有点数就等于一共插入的点数),这样子乘上一个组合数就能得到插入的点是任意点的情形。
时间复杂度 。精细实现就能跑得很快。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int f[100][100],n,sz[100],g[100][100],h[100][100][100],d[100][100][100],C[100][100];
vector<int>v[100];
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int sum;
void pushH(int t,int sp){
for(int i=sum;i>=0;i--)for(int k=0;k<=max(0,t-1);k++)for(int j=0;j+k<=max(0,i-1);j++){
ADD(d[i+1][j][k+1],1ll*(2*t-k)*h[i][j][k]%mod);//put beside one of the same colour.
if(j)ADD(d[i+1][j-1][k],1ll*j*h[i][j][k]%mod);//put inside adjacent same colour pairs.
ADD(d[i+1][j][k],1ll*(i+!!sp-(2*t-k)-j)*h[i][j][k]%mod);//put in a normal position.
}
sum++;
for(int i=0;i<=sum;i++)for(int k=0;k<=max(0,t);k++)for(int j=0;j+k<=max(0,i-1);j++){
h[i][j][k]=d[i][j][k];
if(sp)ADD(g[i][j+k],1ll*d[i][j][k]*C[sp][t+1]%mod);
d[i][j][k]=0;
}
}
void pushG(int x){
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)h[i][j][0]=g[i][j];
for(int t=0;t<x;t++)pushH(t,x);
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)for(int k=0;k<x;k++)h[i][j][k]=0;
}
void dfs(int x,int fa){
sz[x]=1;
for(auto y:v[x])if(y!=fa)dfs(y,x),sz[x]+=sz[y];
for(auto y:v[x])if(y!=fa){
g[0][0]=1,sum=0;
pushG(1);
for(auto Y:v[x])if(y!=Y&&fa!=Y)pushG(sz[Y]);
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)h[i][j][0]=g[i][j];
for(int j=0;j<sz[y];j++){
for(int J=1;J<=sz[y]-j;J++)
for(int i=J;i<=sz[x];i++)ADD(f[x][i],1ll*h[i-J][0][0]*f[y][J]%mod*C[sz[y]-J][j]%mod);
pushH(j,0);
}
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)for(int k=0;k<sz[y];k++)h[i][j][k]=0;
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)g[i][j]=0;
}
// printf("%d:",x);for(int i=0;i<=sz[x];i++)printf("%d ",f[x][i]);puts("");
g[0][0]=1,sum=0;
for(auto y:v[x])if(y!=fa)pushG(sz[y]);
for(int i=1;i<=sz[x];i++)ADD(f[x][i],g[i-1][0]);
for(int i=0;i<=sum;i++)for(int j=0;j<=max(0,i-1);j++)g[i][j]=0;
// printf("%d:",x);for(int i=0;i<=sz[x];i++)printf("%d ",f[x][i]);puts("");
}
int main(){
freopen("seq.in","r",stdin);
freopen("seq.out","w",stdout);
scanf("%d",&n);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
printf("%d\n",f[1][n]);
return 0;
}
XXVII.CF891D Sloth
Observation 1.一个树的完美匹配若存在则唯一。
这个可以用 DP 来模拟一下。
那么如果完美匹配存在,怎么判断一条边是否在完美匹配中呢?很简单,看它左右两侧的节点数的奇偶性。因为存在完美匹配的树的点数必然为偶,所以两侧的奇偶性必然相同。
如果是奇,则这条边必然在匹配中——因为两侧自身都不可能完成匹配。同理,如果是偶,则必不在匹配中。
这样我们便可以想出一种不使用 DP 的判定完美匹配的算法,即所有奇边都没有相同端点。
现在考虑断掉了一条偶边。则连上的边也必然是偶边。故断掉其后存在完美匹配当且仅当这条边两侧都存在完美匹配——这等价于原树有完美匹配。故只要原树可以完美匹配,那就可以枚举偶边计算两侧连通块大小之积的和。
考虑断掉了奇边。
假如原树具有完美匹配,则任两条奇边无相同端点。此时借鉴二分图匹配的想法,新增的边的两个端点间应该是一条交错路,且路径的开头结尾两条边都是奇边。这样在断边再连边后,路径上的所有边的奇偶性翻转,仍然是合法的匹配。
于是我们便需要对所有这样的路径计数上面的奇边数。这个直接在 LCA 处随便统计一下即可。
假如原树不具有完美匹配,则存在数条奇边有相同端点。因为 是偶数,这表明每个点的相邻奇边的数量必然是奇数。又发现,通过删掉并连一条边,我们最多只能影响其中两条奇边侧的子树大小(即,断掉一条奇边侧的某棵奇子树,然后把这条奇边对应的奇子树嫁接到另一棵奇子树),因此如果出现了某个点与大于 条奇边相邻则定无解。
这样所有点就具有 或 条相邻奇边。考虑新增的边的路径。按照我们之前的想法,其应该包含所有与 条奇边相邻的点以及其中至少两条边。其起讫边仍然要求是奇边,且除 -点两侧以外奇偶边交替出现,贡献为其中奇边数。
如何统计?首先先判断所有 -点是否在一条路径上。这样,除了路径上首个和最后一个 -点,其它点在路径上的两条边都已经被确定了。剩下的也可以简单 DP 得出。
另外还有一种易想到的做法是使用换根 DP 处理,设两维表示子树的根是否被匹配、子树中是否有删掉点,然后把合并两个点的操作写成类矩阵乘法的形式。可以调整式子使得上述乘法具有结合律,然后在换根 DP 的时候就可以记录前后缀的和来处理。但是我太懒没有写这种解法。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,sz[500100],num[500100],mx,cnt[500100];
ll sum[500100],res;
vector<int>v[500100];
bool cheodd(int x,int y){return min(sz[x],sz[y])&1;}
void dfs1(int x,int fa){sz[x]=1;for(auto y:v[x])if(y!=fa)dfs1(y,x),sz[x]+=sz[y];}
void dfs2(int x,int fa){
sum[x]=cnt[x]=0;
for(auto y:v[x])if(y!=fa){
dfs2(y,x);
if(cheodd(x,y))res+=(sum[y]+=++cnt[y]),sum[x]+=sum[y],cnt[x]+=cnt[y];
}
for(auto y:v[x])if(y!=fa){
if(cheodd(x,y))continue;
res+=sum[x]*cnt[y]+sum[y]*cnt[x];
}
if(fa&&cheodd(x,fa))for(auto y:v[x])if(y!=fa){
if(cheodd(x,y))continue;
sum[x]+=sum[y],cnt[x]+=cnt[y];
}
}
bool tri[500100];
int X,Y;
void dfs3(int x,int fa){
if(num[x]==3)tri[x]=true;
int tot=0;
for(auto y:v[x])if(y!=fa)dfs3(y,x),tot+=tri[y],tri[x]|=tri[y];
if(num[x]==3&&(!tot||(!fa&&tot==1))){
if(!X)X=x;
else if(!Y)Y=x;
else{puts("0");exit(0);}
}
}
int dfs4(int x,int st,int fa){
if(x==st)return 0;
for(auto y:v[x])if(y!=fa){
int tmp=dfs4(y,st,x);
if(tmp!=-1)return tmp+cheodd(y,x);
}
return -1;
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
if(n&1){puts("0");return 0;}
dfs1(1,0);
for(int i=1;i<=n;i++)for(auto j:v[i])if(cheodd(i,j))num[i]++;
for(int i=1;i<=n;i++)mx=max(mx,num[i]);
if(mx>3){puts("0");return 0;}
if(mx==1){
for(int i=2;i<=n;i++)if(!(sz[i]&1))res+=1ll*sz[i]*(n-sz[i]);
dfs2(1,0);
}else{
for(int i=1;i<=n;i++)if(num[i]==3){dfs3(i,0);break;}
if(!Y){
dfs2(X,0),res=0;
vector<ll>u;
for(auto y:v[X])if(cheodd(y,X))u.push_back(y);
assert(u.size()==3);
for(auto i:u)for(auto j:u)if(i!=j)res+=sum[i]*cnt[j];
}else{
// printf("%d %d\n",X,Y);
vector<int>ux,uy;
int fx,fy;
dfs1(X,0);for(auto x:v[Y])if(sz[x]>sz[Y])fy=x;
dfs1(Y,0);for(auto y:v[X])if(sz[y]>sz[X])fx=y;
dfs2(X,fx);for(auto y:v[X])if(y!=fx&&cheodd(y,X))ux.push_back(y);
dfs2(Y,fy);for(auto x:v[Y])if(x!=fy&&cheodd(x,Y))uy.push_back(x);
// for(auto i:ux)printf("%d:%d,%d\n",i,sum[i],cnt[i]);puts("");
// for(auto i:uy)printf("%d:%d,%d\n",i,sum[i],cnt[i]);puts("");
res=0;
ll tot=0;
for(auto i:ux)for(auto j:uy)res+=sum[i]*cnt[j]+sum[j]*cnt[i],tot+=1ll*cnt[i]*cnt[j];
int tmp=dfs4(X,Y,0);
res+=tot*tmp;
}
}
printf("%lld\n",res);
return 0;
}
XXVIII.[POI2017]Sabotaż
设 表示最小的不可能使 子树中全体节点背叛的值。
对于叶节点,令 。对于其它节点,令 。
这个式子很显然,因为儿子想要拉拢父亲,首先自身要叛变,其次自身的叛变要能影响到父亲,故两者是取 ;而父亲可以被最优的一个儿子拉拢,因此是取 。
答案就是全体子树大小不大于 且父亲节点子树大小大于 的 (明显这些点中任何一个都不能有机会拉拢父亲)的 。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,fa[500100],sz[500100];
double bet[500100],res;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)sz[i]=1,bet[i]=1;
for(int i=2;i<=n;i++)scanf("%d",&fa[i]),bet[fa[i]]=0;
for(int i=n;i>=2;i--)sz[fa[i]]+=sz[i];
// for(int i=1;i<=n;i++)printf("%d ",sz[i]);puts("");
for(int i=n;i>=2;i--)bet[fa[i]]=max(bet[fa[i]],min(bet[i],1.0*sz[i]/(sz[fa[i]]-1)));
// for(int i=1;i<=n;i++)printf("%lf ",bet[i]);puts("");
for(int i=2;i<=n;i++){
if(sz[i]>m||sz[fa[i]]<=m)continue;
res=max(res,min(bet[i],1.0*sz[i]/(sz[fa[i]]-1)));
}
printf("%.10lf\n",res);
return 0;
}
XXIX.PERIODNI - Periodni
建立笛卡尔树,然后每个节点代表的部分是一个不规则图形,左右边界是其作为最大值的区间,下边界是其父亲值。设 表示 节点在上述图形中选择 位置的方案数。
节点比起其左右儿子新增的部分是左右边界为区间、下边界为父亲值、上边界为其自身值的矩形。在矩形中填数是简单的,直接组合数什么瞎算算即可。
从左右儿子合并上去的部分可以使用树上背包,复杂度可以做到 。但是本人偷懒,写的是 的。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
const int N=1e6;
int n,m,a[510],ls[510],rs[510],L[510],R[510],stk[510],tp,rt,f[510][510],fac[1001000],inv[1001000];
void dfs(int x,int lim){
if(ls[x])dfs(ls[x],a[x]);
if(rs[x])dfs(rs[x],a[x]);
if(!ls[x]&&!rs[x])f[x][0]=1;
else if(!ls[x]||!rs[x])for(int i=0;i<=m;i++)f[x][i]=f[ls[x]+rs[x]][i];
else for(int i=0;i<=m;i++)for(int j=0;i+j<=m;j++)f[x][i+j]=(1ll*f[ls[x]][i]*f[rs[x]][j]+f[x][i+j])%mod;
int u=a[x]-lim,v=R[x]-L[x]+1;
// printf("%d:",x);for(int i=0;i<=m;i++)printf("%d ",f[x][i]);puts("");
for(int i=v;i;i--)for(int j=1;j<=min(min(u,v),i);j++)
f[x][i]=(1ll*f[x][i-j]*fac[u]%mod*inv[u-j]%mod*fac[v-i+j]%mod*inv[v-i]%mod*inv[j]+f[x][i])%mod;
// printf("%d:",x);for(int i=0;i<=m;i++)printf("%d ",f[x][i]);puts("");
}
int main(){
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
while(tp&&a[stk[tp]]>a[i])R[stk[tp]]=i-1,ls[i]=stk[tp],tp--;
if(tp)rs[stk[tp]]=i;L[i]=stk[tp]+1,stk[++tp]=i;
}
while(tp)R[stk[tp--]]=n;rt=stk[1];
dfs(rt,0);
printf("%d\n",f[rt][m]);
// for(int i=1;i<=n;i++)printf("[%d,%d]",L[i],R[i]);puts("");
// for(int i=1;i<=n;i++)printf("[%d,%d]",ls[i],rs[i]);puts("");
return 0;
}
XXX.CF512D Fox And Travelling
观察到环上——进一步说,边双中——所有点都不会被选择。于是我们可以作一遍类拓扑排序的算法,“剥离”所有边缘度数为 的点。
这时候,我们发现我们得到了一堆树,一些是定根的,一些不是。
拓扑序什么的在定根时是可以简单树上背包计算的。不定根时,我们考虑随便找个根,然后将所有情形分成两类:所有父亲均在儿子后删去的类型,以及其它类型。前者和定根树相同,后者我们一定可以找到一个点,其父亲在其之前删去,且所有子树中点都在其后删除。这样我们就可以发现,其父亲侧子树中所有点都会被删去,那么就暴力扫一遍 DP 过去即可。
时间复杂度 。应该有优化空间,但我懒得管了。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+9;
int n,m,sz[110],f[110],C[110][110];
int v[110][110],deg[110];
vector<int>u[110];
queue<int>q;
int g[110][110],h[110];
void dfs1(int x,int fa){
g[x][0]=1;
for(auto y:u[x])if(y!=fa){
dfs1(y,x);
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++)
h[i+j]=(1ll*g[x][i]*g[y][j]%mod*C[i+j][j]+h[i+j])%mod;
sz[x]+=sz[y];
for(int i=0;i<=sz[x];i++)g[x][i]=h[i],h[i]=0;
}
g[x][sz[x]+1]=g[x][sz[x]],sz[x]++;
if(fa==-1){
for(int i=0;i<=n;i++)for(int j=0;i+j<=n;j++)
h[i+j]=(1ll*g[x][i]*f[j]%mod*C[i+j][j]+h[i+j])%mod;
for(int i=0;i<=n;i++)f[i]=h[i],h[i]=0;
}
deg[x]=-1;
}
int zs[110],d[110],p[110];
void dfs3(int x,int fa){
zs[x]=0,d[x]=1;
for(auto y:u[x])if(y!=fa)dfs3(y,x),d[x]=1ll*C[zs[x]+zs[y]][zs[y]]*d[x]%mod*d[y]%mod,zs[x]+=zs[y];
zs[x]++;
}
void dfs2(int x,int fa){
g[x][0]=1;
for(auto y:u[x])if(y!=fa){
dfs2(y,x);
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y];j++)
h[i+j]=(1ll*g[x][i]*g[y][j]%mod*C[i+j][j]+h[i+j])%mod;
sz[x]+=sz[y];
for(int i=0;i<=sz[x];i++)g[x][i]=h[i],h[i]=0;
}
g[x][sz[x]+1]=g[x][sz[x]],sz[x]++;
int fz=0,fv=1;
if(fa!=-1)dfs3(fa,x),fz=zs[fa],fv=d[fa];
for(int i=0;i<sz[x];i++)p[i+fz]=(1ll*g[x][i]*fv%mod*C[i+fz][i]+p[i+fz])%mod;
p[sz[x]+fz]=(1ll*g[x][sz[x]]*fv%mod*C[sz[x]+fz-1][fz]+p[sz[x]+fz])%mod;
// for(int i=0;i<=n;i++)printf("%d ",p[i]);puts("");
if(fa==-1){
// for(int i=0;i<=n;i++)printf("%d ",p[i]);puts("");
for(int i=0;i<=n;i++)for(int j=0;i+j<=n;j++)
h[i+j]=(1ll*p[i]*f[j]*C[i+j][j]+h[i+j])%mod;
for(int i=0;i<=n;i++)f[i]=h[i],h[i]=p[i]=0;
}
deg[x]=-1;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x][y]=v[y][x]=1,deg[x]++,deg[y]++;
for(int i=1;i<=n;i++)if(deg[i]<=1)q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
for(int y=1;y<=n;y++){
if(!v[x][y])continue;
if((--deg[y])==1)q.push(y);
}
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(v[i][j]&°[i]<=1&°[j]<=1)u[i].push_back(j);
// for(int i=1;i<=n;i++)printf("%d ",deg[i]);puts("");
f[0]=1;
for(int i=1;i<=n;i++)if(deg[i]==1)dfs1(i,-1);
for(int i=1;i<=n;i++)if(deg[i]==0)dfs2(i,-1);
for(int i=0;i<=n;i++)printf("%d ",f[i]);puts("");
return 0;
}
XXXI.CF762F Tree nesting
做烦了。
首先肯定考虑找一个点定作 的根。然后枚举这个点到底匹配了 上哪个点,并用 DP 求出此时合法连通块数。
找哪个点呢?因为我们不想重复计数,所以按照自同构理论的套路,我们选择 的重心。这样,一个连通块就会被且仅被统计 的自同构个数次。
于是就 DP。设 表示点 与 中的节点 匹配的方案数。转移枚举儿子与哪个点配,故需要一个状压的辅助 DP。
然后想了想怎么转移,发现还得换个根。因为状压 DP 可以简单合并,于是直接记录前后缀的 DP 值然后进行合并即可。又因为空间只有 256M,所以开不下 的前后缀 DP 数组,通过对不同的 合并转移省掉了一个 。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
const int bs=13;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int n,m,res;
vector<int>u[1010],v[20],vec;
int f[1010][20],g[1010][20],h[1<<12],deg[20],frm[20],id[20],rt;
int fav[1010][20][20];
int p[1010][1<<12],q[1010][1<<12];
void calch(int x,int i){
int lim=1<<deg[i];
for(int k=0;k<lim;k++)h[k]=0;h[0]=1;
for(auto y:u[x])for(int k=lim-1;k>=0;k--)for(int j=0;j<deg[i];j++)
if(!(k&(1<<j)))ADD(h[k|(1<<j)],1ll*h[k]*f[y][v[i][j]]%mod);
// printf("H:");for(int k=0;k<lim;k++)printf("%d ",h[k]);puts("");
}
void dfs1(int x){
vector<int>w;
for(auto y:u[x])u[y].erase(find(u[y].begin(),u[y].end(),x)),dfs1(y);
for(int i=1;i<=m;i++)calch(x,i),f[x][i]=h[(1<<deg[i])-1];
// printf("%d:",x);for(int i=1;i<=m;i++)printf("%d ",f[x][i]);puts("");
}
void dfs2(int x){
// printf("%2d:",x);for(int i=1;i<=m;i++)printf("%2d ",f[x][i]);puts("");
for(int i=1;i<=m;i++)ADD(f[x][i],g[x][i]);
if(u[x].empty())return;
for(auto i:vec){
if(i==rt)continue;
int lim=(1<<deg[i]);
p[*u[x].begin()][0]=1;for(int j=0;j<deg[i];j++)p[*u[x].begin()][1<<j]=fav[x][i][j];
for(auto y=u[x].begin();next(y)!=u[x].end();y++){
for(int j=0;j<deg[i];j++)for(int k=0;k<lim;k++)if(!(k&(1<<j)))
ADD(p[*next(y)][k|(1<<j)],1ll*p[*y][k]*f[*y][v[i][j]]%mod);
for(int k=0;k<lim;k++)ADD(p[*next(y)][k],p[*y][k]);
}
q[*u[x].rbegin()][0]=1;
for(auto y=u[x].rbegin();next(y)!=u[x].rend();y++){
for(int j=0;j<deg[i];j++)for(int k=0;k<lim;k++)if(!(k&(1<<j)))
ADD(q[*next(y)][k|(1<<j)],1ll*q[*y][k]*f[*y][v[i][j]]%mod);
for(int k=0;k<lim;k++)ADD(q[*next(y)][k],q[*y][k]);
}
for(auto y:u[x]){
int sum=0;
for(int k=0;k<lim;k++)ADD(sum,1ll*p[y][k]*q[y][(lim-1)^k]%mod);
calch(y,frm[i]);
fav[y][frm[i]][id[i]]=sum;
ADD(g[y][frm[i]],1ll*h[(1<<deg[frm[i]])-1-(1<<id[i])]*sum%mod);
for(int k=0;k<lim;k++)p[y][k]=q[y][k]=0;
}
}
for(auto y:u[x])dfs2(y);
}
namespace isomor{
ll hs[20];
int pro=1,fac[20];
int sz[20],msz[20];
void grt(int x,int fa){
sz[x]=1;
for(auto y:v[x])if(y!=fa)grt(y,x),sz[x]+=sz[y],msz[x]=max(msz[x],sz[y]);
msz[x]=max(msz[x],m-sz[x]);
if(msz[rt]>msz[x])rt=x;
}
void gsz(int x){
vec.push_back(x);
sz[x]=1;
for(auto y:v[x])v[y].erase(find(v[y].begin(),v[y].end(),x)),gsz(y),sz[x]+=sz[y];
for(int i=0;i<v[x].size();i++)frm[v[x][i]]=x,id[v[x][i]]=i;
}
void aim(int x,int fa){
vector<ll>w;
for(auto y:v[x])if(y!=fa)aim(y,x),w.push_back(hs[y]);
sort(w.begin(),w.end());
for(int l=0,r=0;l<w.size();l=r){
while(r<w.size()&&w[r]==w[l])r++;
pro=1ll*pro*fac[r-l]%mod;
}
hs[x]=1;for(auto i:w)hs[x]=hs[x]*bs+i;
}
}using namespace isomor;
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),u[x].push_back(y),u[y].push_back(x);
scanf("%d",&m);
for(int i=1,x,y;i<m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
fac[0]=1;for(int i=1;i<=m;i++)fac[i]=1ll*fac[i-1]*i%mod;
msz[rt=0]=m,grt(1,0),aim(rt,0),gsz(rt);
for(int i=1;i<=m;i++)if((sz[i]<<1)==m){
int qwq=pro;
aim(rt,i),aim(i,rt);
pro=qwq;
if(hs[rt]==hs[i])(pro<<=1)%=mod;
break;
}
for(int i=1;i<=m;i++)deg[i]=v[i].size();
// for(int i=1;i<=m;i++)printf("%d ",deg[i]);puts("");
dfs1(1),dfs2(1);
for(int i=1;i<=n;i++)ADD(res,f[i][rt]);
printf("%d\n",1ll*res*ksm(pro)%mod);
return 0;
}
XXXII.CF1534H Lost Nodes
首先考虑若初始给出的节点是 时的最小询问次数。设 表示你已经确定一个点藏在了 的子树中(当然,你已经在其中使用了一次询问,但是这次询问不会被计入 中),此时要找到答案需要的最少操作次数。对于叶子,显然有 。
考虑一个非叶子节点的 DP 值应该怎样计算。我们可以知道其所有儿子的 DP 值;这之后,我们肯定希望先去问 DP 值更大的儿子。这就找到了转移式:令 为递减排序的儿子的 DP 数组,则 。
在求出 DP 数组后,我们考虑根处的答案应该如何统计。我们仍然是按照 DP 值从大往小去问儿子。假如问到儿子 时,两个点都被确定了,则总询问次数为 ,其中 是另一个确定的位置,且 。这显然会在 为全体儿子的 DP 值的最大值时取到最大值。故我们只需枚举 即可。
需要注意的是,我们除了根是叶子的情形外,都不需要考虑有一个藏起来的点是根自身的情形:因为根自身只有在所有儿子都被排除后才会被确定,但是确定最后一个儿子如果成功需要 次,失败需要 次,无论如何成功都不劣于失败。而叶子时,直接有答案等于其唯一一个连边点的 DP 值加一。
现在考虑交互的过程。只需要像链剖分一样,为每个点找到其 DP 值最大的儿子作为重儿子,然后询问重链的底即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int query(int x){printf("? %d\n",x),fflush(stdout),scanf("%d",&x);return x;}
int n,f[100100],res,son[100100];
vector<int>v[100100],u[100100];
bool cmp(int i,int j){return f[i]>f[j];}
void dfs1(int x,int fa){
son[x]=x,u[x].clear(),f[x]=0;
for(auto y:v[x])if(y!=fa)dfs1(y,x),u[x].push_back(y);
if(u[x].empty())return;
sort(u[x].begin(),u[x].end(),cmp);
son[x]=son[u[x][0]];
for(int i=0;i<u[x].size();i++)f[x]=max(f[x],i+f[u[x][i]]);
}
void dfs2(int x,int fa){
// printf("%d:",x);
for(int i=0,mx=0;i<u[x].size();i++){
// printf("[%d,%d]",u[x][i],f[u[x][i]]);
res=max(res,i+f[u[x][i]]+mx+1);
mx=max(mx,f[u[x][i]]);
}//puts("");
int mx=-1,smx=-1,mxp=-1,smxp=-1;
for(int i=0;i<u[x].size();i++){
if(mx<f[u[x][i]]+i)smx=mx,smxp=mxp,mx=f[u[x][i]]+i,mxp=i;
else if(smx<f[u[x][i]]+i)smx=f[u[x][i]]+i,smxp=i;
}
mx=max(mx,0),smx=max(smx,0);
int tf=f[x];
for(int i=0,y;i<u[x].size();i++)if((y=u[x][i])!=fa){
if(i==mxp)f[x]=smx-(smxp>i);
else f[x]=mx-(mxp>i);
u[y].insert(lower_bound(u[y].begin(),u[y].end(),x,cmp),x);
dfs2(y,x);
}
f[x]=tf;
}
int search(int x){
for(int i=1,y;i<u[x].size();i++){
y=query(son[u[x][i]]);
if(y!=x)return search(y);
}
return x;
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
if(n!=1)dfs1(1,0),dfs2(1,0);
printf("%d\n",res),fflush(stdout);
int RT;scanf("%d",&RT);
if(n==1){printf("! %d %d\n",1,1),fflush(stdout);return 0;}
dfs1(RT,0);
int a=-1,b=-1;
for(auto x:u[RT]){
x=query(son[x]);
if(x!=RT)swap(a,b),a=search(x);
if(b!=-1)break;
}
if(a==-1)a=RT;if(b==-1)b=RT;
printf("! %d %d\n",a,b),fflush(stdout);
return 0;
}
XXXIII.CF1060F Shrinking Tree
我们钦定一个点最终存活,把它设为根。
考虑应该怎么设计状态:设 表示,通过神秘措施,根节点已经与 合并了,此时的概率。
考虑怎么合并一个节点和它的儿子。因为我们不知道连接二者的边的排名,所以无法合并。
常见的想法之一是,枚举子树中一条边并钦定其接下来被合并。但是这样做无法眷顾中间那些边。
于是我们考虑扩张状态的定义:现在令 表示仅考虑节点 子树中的所有边,当根移动到 时,所有边中还剩 条边未合并的概率和。因为这个概率和事实上是指考虑全部 种删边顺序,它们最终让 幸存的概率和,所以最终答案要除以 。
考虑合并节点 和它的一个儿子 。
我们首先要把 这条边给引入 的子树。于是记一个辅助状态 表示该边被引入后 的子树的状态。对于一个 ,考虑枚举 边是在倒数第 步被删掉。
若 ,这说明我们在意 边的序号,因此要乘上 的系数,有 。
否则,,这说明我们不在意 边的序号,但是因为保证最终恰幸存 条边,所以有 。
那么现在我们合并两个节点的状态吧!显然在合并后,幸存的边和已经被合并的边二者是分开的。那么合并 与 ,方案数就是 。
大力转移,复杂度三方;对每个节点各自 DP 一次,复杂度四方。
代码:
#include<bits/stdc++.h>
using namespace std;
double C[60][60],fac[60],f[60][60],g[60],h[60];
int n,sz[60];
vector<int>v[60];
void dfs(int x,int fa){
f[x][0]=1,sz[x]=0;
for(auto y:v[x])if(y!=fa){
dfs(y,x);
for(int i=0;i<=sz[y]+1;i++)
for(int j=1;j<=sz[y]+1;j++)
if(j<=i)g[i]+=0.5*f[y][j-1];
else g[i]+=f[y][i];
// printf("(%d-%d)\n",x,y);
// for(int i=0;i<=sz[y]+1;i++)printf("%lf ",g[i]);puts("");
for(int i=0;i<=sz[x];i++)for(int j=0;j<=sz[y]+1;j++)
h[i+j]+=C[i+j][j]*C[sz[x]-i+sz[y]+1-j][sz[x]-i]*f[x][i]*g[j];
sz[x]+=sz[y]+1;
for(int i=0;i<=sz[x];i++)f[x][i]=h[i],h[i]=g[i]=0;
// for(int i=0;i<=sz[x];i++)printf("%lf ",f[x][i]);puts("");
}
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i;
for(int i=1;i<=n;i++)dfs(i,0),printf("%lf\n",f[i][n-1]/fac[n-1]);
return 0;
}
XXXIV.CF1517F Reunion
考虑计算出最大等级大于等于 的概率。对于所有 将这个概率求和,即可得到所有最大等级之和。
枚举 。
考虑容斥:用钦定一个点的 -邻域的总概率,减去钦定两个点的概率,加上钦定三个点的概率……
现在设 表示以 为根的子树中存在一个邻域,会影响到所有与 距离不超过 的点,且钦定的总邻域数的奇偶性是 0/1 时的方案数。转移是简单的……吗?
并不是,因为你没法良好地考虑父亲的邻域对儿子的影响。
所以我们换一种思路。考虑一个点的邻域何时不合法:当且仅当其邻域中存在一个死掉的点。
换句话说,一个死掉的点会污染其邻域中的所有点。
我们要计算存在一个点未被任何点污染的状态;这显然是不好做的,我们把它取反,转成不存在这样点的个数,也即所有点都被至少一个点污染。
那么这就已经被转成经典树形 DP 问题了。设 表示 子树被染色的深度是 的方案数,其中 表明子树中的点有余力去覆盖子树外的点, 表明子树中的点需要靠子树外的点支持。
DP 是简单的;但是暴力实现的复杂度是四方。
一种想法是用前缀和什么的把转移拆开,这样可以直接变成三方。或者,观察到一个状态中非零位只有 个,于是用数据结构暴力维护全体非零位进行转移即可用树上背包的复杂度分析得到复杂度是三方的。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int inv2=499122177;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int n,R,res;
map<int,int>f[310],g;
vector<int>v[310];
void dfs(int x,int fa){
f[x].clear();
f[x][-1]=f[x][R]=1;
for(auto y:v[x])if(y!=fa){
dfs(y,x);
for(auto i:f[x])for(auto j:f[y]){
int val=1ll*i.second*j.second%mod;
if(i.first>=0&&j.first>=0){ADD(g[max(i.first,j.first-1)],val);continue;}
if(i.first<0&&j.first<0){ADD(g[min(i.first,j.first-1)],val);continue;}
if(i.first<0){
if(j.first>=-i.first){ADD(g[j.first-1],val);continue;}
else{ADD(g[i.first],val);continue;}
}
if(j.first<0){
if(i.first>=-j.first){ADD(g[i.first],val);continue;}
else{ADD(g[j.first-1],val);continue;}
}
}
f[x]=g,g.clear();
}
// printf("%d:",x);
// for(auto i:f[x])printf("[%d,%d]",i.first,i.second);puts("");
}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(R=1;R<n;R++){
// printf("%d:\n",R);
dfs(1,0);
for(auto x:f[1])if(x.first>=0)(res+=x.second)%=mod;
}
// printf("%d\n",res);
for(int i=1;i<=n;i++)res=1ll*res*inv2%mod;
printf("%d\n",(n-1+mod-res)%mod);
return 0;
}
XXXV.拯救
有一张 点 边的无向简单连通图。依次执行 次操作,为以下三种之一:
- 删去一条未被删去的边。
- 为一个 从未 被打过标记的点打上标记。
- 撤销一个点的标记。
你在 时刻的操作执行之前,向节点 上放置了一个指针。
指针可以经由未被删去的边移动,移动不消耗时间。当指针初次移动到一个存在现有标记的点时,会对答案产生 的贡献。多次访问不产生贡献。
你还可以执行 不超过一次 的特殊操作:在任意时刻,不改变你的指针所在位置以及当前答案,并将局面 变更 到操作 执行之前的状态,其中 , 是给定常数。之后,从操作 开始依次执行操作。你仍然可以移动指针并对答案产生贡献,但是再次访问局面变更之前就已经被访问到的点并不会产生新贡献。
回答 次 完全独立 的 询问。
数据范围:。
首先考虑你指针的移动范围。然后接着发现,假如指针在 时刻位于某个连通块中,且 时刻该连通块中的某条边断裂并分成两个小连通块,则指针只能往其中一个连通块中去。
这显然构成一个树形结构。事实上,这是边关于其删除时间的 Kruskal 重构树。用冰茶姬即可在阿克曼时间内建出之。
然后考虑原图中一个点对答案的贡献:其在某个时刻开始可以产生贡献,在某个时刻开始不再产生贡献。放到我们的重构树上,就是一条深度递增的路径。我们称这些路径为 贡献路径。
假如没有特殊操作,则我们的指针显然是从 时刻的 节点在重构树上对应节点 出发,到某个叶子的路径。称这条路径为 指针路径。则,所有与指针路径有交的贡献路径(可能在路径端点处稍微要讨论一下有没有交集什么的)都会产生贡献。
考虑一个 DP。 表示指针在 且接下来往子树中走,能获得的最大收益。 DP 显然是简单的:一条贡献路径的贡献在其与指针路径交集的最深处统计,则 可以从某个儿子的 转移来,还要额外计入那些从 开始的贡献路径,以及从另一个儿子出发延伸到 的路径。这两个都是可以简单维护的。
现在考虑特殊操作。显然,特殊操作必然是自 时刻的 节点在重构树上对应节点 出发最优——因为向父亲多前进一步必然不更劣。
那么现在就要稍微分类讨论一下了。两段指针序列的贡献可以怎样表示呢?
- 自 出发的路径在 子树中某个叶子处结束。( 的定义在上文中已出现)
- 自 出发的路径没有进入 子树中某个叶子。
前者就相当于自 出发两条路径,然后额外统计 的贡献即可。 的贡献是简单的, 子树中两条路径亦可通过定义 表示之,简单 DP 预处理出。
后者考虑枚举路径中最后一个是 祖先的点,设为 。则 必然在 路径上。则此时的答案是由 子树中选一条路径(显然,此时取 最优)、 非 侧儿子所在子树中选一条路径(即为该儿子的 )、 路径(这个贡献是简单的),以及自 的 侧儿子向上的路径的贡献。但是,需要注意的是,这些路径中 不能包含来自 子树的路径,也即同时包含 两点的路径不应该计算,因为它们已经在 中被算过了。
因为 ,所以一切都可以暴力。Kruskal 重构树上定位可以倍增。假定 同阶,复杂度 。
代码写的是在线做法,不过相信离线做法要好写一百万倍。另外还有一种我觉得能大大减少细节的方式就是 拆点,对于 Kruskal 重构树上的每个点,关于涉及到它的路径的起讫时间拆开。虽然常数会有些大,但应该更好写。另外可能这么做会产生大量时刻相同的点导致不能暴力跳父亲,但是可以把跳父亲换成首个权值减小的祖先,应该就可以了。
但是两种优化我都没有用。因此可能会被细节叉掉,所以希望不要拿我的代码去拍(bushi
代码:
include<bits/stdc++.h>
using namespace std;
const int N=500100;
int n,m,K,q,C,cnt;
int X[N],Y[N];
int op[N],pos[N],fa[N<<1],dsu[N<<1],ch[N<<1][2],tim[N<<1],anc[N<<1][20];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
vector<int>ics[N<<1];
int dcs[N<<1],num[N<<1];//dcs:the things available at x but unavailable at fa.
bool on[N],era[N];
int f[N<<1],g[N<<1];
int ext[N<<1][26],txe[N][26];
int stm[N],edp[N],dep[N<<1];
int stk[N<<1],tp;
void dfs(int x){
reverse(ics[x].begin(),ics[x].end());
dep[x]=dep[fa[x]]+1;
stk[++tp]=x;
static int qwq[26];
memset(qwq,0,sizeof(qwq));
for(auto i:ics[x]){
int P=lower_bound(stk+1,stk+tp+1,edp[i],[](int x,int y){return x>y;})-stk;
qwq[min(tp-P,C+1)]++;
for(int j=0;j<=C+1;j++)txe[i][j]=qwq[j];
for(int j=C;j>=0;j--)txe[i][j]+=txe[i][j+1];
for(int j=P+1;j<=P+C+1&&j<=tp;j++)ext[stk[j]][j-P]++;
}
anc[x][0]=fa[x];for(int i=1;i<20;i++)anc[x][i]=anc[anc[x][i-1]][i-1];
// printf("%d->%d:%d\n",fa[x],x,ics[x].size());
if(x>n){
for(int i=0;i<2;i++)dfs(ch[x][i]);
for(int i=0;i<2;i++)
f[x]=max(f[x],f[ch[x][i]]+num[ch[x][i^1]]),
g[x]=max(g[x],g[ch[x][i]]+num[ch[x][i^1]]),
num[x]+=num[ch[x][i]];
g[x]=max(g[x],f[ch[x][0]]+f[ch[x][1]]);
f[x]+=ics[x].size(),g[x]+=ics[x].size(),num[x]+=ics[x].size()-dcs[x];
}else f[x]=g[x]=ics[x].size(),num[x]=ics[x].size()-dcs[x];
for(int i=1;i<=C+1;i++)ext[x][i]+=ext[x][i-1];
// printf("%d:",x);for(int i=1;i<=C;i++)printf("%d ",ext[x][i]);puts("");
tp--;
// printf("%d:%d %d %d\n",x,f[x],g[x],num[x]);
}
int findanc(int x,int t){
for(int i=19;i>=0;i--)if(anc[x][i]&&tim[anc[x][i]]>=t)x=anc[x][i];
return x;
}
int main(){
freopen("save.in","r",stdin);
freopen("save.out","w",stdout);
scanf("%d%d%d%d%d",&n,&m,&K,&q,&C),cnt=n;
for(int i=1;i<=m;i++)scanf("%d%d",&X[i],&Y[i]);
for(int i=1;i<=K;i++){
scanf("%d%d",&op[i],&pos[i]);
if(op[i]==1)era[pos[i]]=true;
if(op[i]==2)on[pos[i]]=true;
if(op[i]==3)on[pos[i]]=false;
}
for(int i=1;i<=n;i++)if(on[i])ics[i].push_back(i),stm[i]=K+1;
for(int i=1;i<=n;i++)dsu[i]=i;
for(int i=1;i<=m;i++)if(!era[i]){
int x=find(X[i]),y=find(Y[i]);
if(x==y)continue;
int z=++cnt;
tim[z]=K+1,ch[z][0]=x,ch[z][1]=y;
fa[x]=fa[y]=dsu[x]=dsu[y]=dsu[z]=z;
}
for(int i=K;i;i--){
if(op[i]==3){stm[pos[i]]=i,ics[find(pos[i])].push_back(pos[i]);continue;}
if(op[i]==2){dcs[edp[pos[i]]=find(pos[i])]++;continue;}
int x=find(X[pos[i]]),y=find(Y[pos[i]]);
if(x==y)continue;
int z=++cnt;
tim[z]=i,ch[z][0]=x,ch[z][1]=y;
fa[x]=fa[y]=dsu[x]=dsu[y]=dsu[z]=z;
}
dfs(cnt);
for(int i=1,x,y;i<=q;i++){
scanf("%d%d",&x,&y);
x=findanc(x,y);int z=findanc(x,y-C);
int ret=g[x];
int qwq=f[x]-num[x];
stm[0]=y;
auto it=lower_bound(ics[x].begin(),ics[x].end(),0,[](int x,int y){return stm[x]<stm[y];});
qwq-=it-ics[x].begin();
int P=0;if(it!=ics[x].begin())P=*--it;
for(int X,Y,Z=x;x!=z;){
X=x,x=fa[x],Y=ch[x][0]^ch[x][1]^X;
ret=max(ret+num[Y],num[X]+f[Y]+qwq+txe[P][dep[Z]-dep[x]]+ext[Z][dep[Z]-dep[x]-1])+ics[x].size();
}
stm[0]=y-C;
ret-=lower_bound(ics[x].begin(),ics[x].end(),0,[](int x,int y){return stm[x]<stm[y];})-ics[x].begin();
printf("%d\n",ret);
}
return 0;
}
XXXVI.CF1799H Tree Cutting
将流程反转,变成自一个连通块开始,每次增一个连通块。我们认为最开始的连通块是根。
所有的连通块成一个树,枚举所有可能的树的结构(共 种),就知道每次增一个连通块时,新增连通块的根的子树大小(即为所有后来连到该连通块下的新连通块大小之和)。之后 DP 是简单的。
一个想法是枚举每个点,计数其位于根连通块中的场合,然后减去每条边在根连通块中的场合,即为点数减边数的 trick。但是这就要 的复杂度,外面还要再枚举树的结构,无法通过。
事实上,强制 为根,然后枚举 所在连通块对应树中的哪个连通块即可。
DP 是简单的。时间复杂度……
上面的东西在放屁。DP 是困难的,必须要有状压。
另一种想法是,在子树中状压维护已选择的连通块,然后还是点减边,转移时换个根,但是比较难写。
一个比较好的想法是,在断每条边时,抉择保留的是上面还是下面,但断边总是需要满足连通块限制。
时间复杂度 。用集合幂级数技巧可以做到 ,但在本题中不需要。
III.DAG 上 DP
DAG 上 DP 往往要利用拓扑序来排除后效性。
I.[POI2006]PRO-Professor Szu
我要举报……本题数据与题面不符(事实上我已经举报了……),会有到不了主楼的情形,要特别考虑。
思路很简单,我们跑SCC缩点。假如一个SCC内部有自环,显然可以一直绕自环,故答案是无限;同时,所有可以走到该SCC的其它点答案都是无限。
于是我们反向所有边,从终点开始拓扑排序,传递无限的情形,并进行DP(设表示从节点到终点的路径数量即可)。注意要先把所有从到不了的SCC连出的边删去,同时不应该考虑终点自身。
代码:
#include<bits/stdc++.h>
using namespace std;
const int lim=36500;
int n,m,dfn[1001000],low[1001000],tot,f[1001000],col[1001000],c,in[1001000],res,cnt;
bool inf[1001000];
vector<int>v[1001000],u[1001000];
stack<int>s;
void Tarjan(int x){
dfn[x]=low[x]=++tot,s.push(x);
for(auto y:v[x]){
if(!dfn[y])Tarjan(y),low[x]=min(low[x],low[y]);
else if(!col[y])low[x]=min(low[x],dfn[y]);
}
if(low[x]<dfn[x])return;
c++;
int y;
do y=s.top(),s.pop(),col[y]=c;while(y!=x);
}
queue<int>q;
int main(){
scanf("%d%d",&n,&m),n++;
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[y].push_back(x);
for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i);
for(int i=1;i<=n;i++)for(auto j:v[i])if(col[i]!=col[j])u[col[i]].push_back(col[j]),in[col[j]]++;else inf[col[i]]=true;
for(int i=1;i<=c;i++)if(!in[i])q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
if(x==col[n])continue;
inf[x]=f[x]=0;
for(auto y:u[x])if(!--in[y])q.push(y);
}
f[col[n]]=!inf[col[n]],q.push(col[n]);
while(!q.empty()){
int x=q.front();q.pop();
for(auto y:u[x]){
if(!inf[y])f[y]+=f[x],inf[y]|=inf[x];
if(f[y]>lim)f[y]=0,inf[y]=true;
if(!--in[y])q.push(y);
}
}
for(int i=1;i<=c;i++){
if(inf[i])res=lim+1;
else res=max(res,f[i]);
}
if(res>lim){
puts("zawsze");
for(int i=1;i<n;i++)cnt+=inf[col[i]];
printf("%d\n",cnt);
for(int i=1;i<n;i++)if(inf[col[i]])printf("%d ",i);
}else{
printf("%d\n",res);
for(int i=1;i<n;i++)cnt+=(f[col[i]]==res);
printf("%d\n",cnt);
for(int i=1;i<n;i++)if(f[col[i]]==res)printf("%d ",i);
}
return 0;
}
II.CF1466H Finding satisfactory solutions
我们要求出合法的排列组数。
这首先要找到一个对于某个排列组 ,判定某个排列 是否合法的算法。
考虑假设另一个排列 比 更优。则,我们建一张图,连边所有的 。
则需要存在一个环,满足对于其中所有的 都有 相对于 来说不比 更劣,且存在一个 满足 更优。
所以我们对于排列 ,连一条 白边 ,然后再连若干条 黑边 ,其中 在 中排在 前面。
我们发现,合法的排列组 应该满足建出的这张图 不存在包含黑边的环。
换句话说,黑边自身成一张 DAG,且加上全体白边后其仍是 DAG。
考虑白边的影响:我们可以对白环 缩点。假如缩点后的图不是 DAG,则必然可以从这个图上的任一环构造得出原图中的一个环。则我们只需对缩点后的 DAG 计数即可。
于是我们便将问题转化成了一个 DAG 计数问题。
按照套路,我们应该每次剥掉若干入度为 的点。则目前尚未被剥掉的点构成原图的一个子图。
注意到大小相同的白环等价。故我们可以令一个数组 , 表示长为 的白环数量。然后本质不同的子图数量就只有 个。在 时,这一数量不超过 。
然后就直接 DP 即可。钦定若干个点入度为 ,容斥即可。容斥的时候要计算一个大小为 的点集连向的边全都在另一个大小为 的点集内的方案数;应注意按照实际意义(即其对应的排列数量)来解释。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int c[110],p[110],n,m=1,C[110][110],fac[110],g[110][110];
bool vis[110];
int sta[2010][110],f[2010];
int main(){
scanf("%d",&n);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++)(g[i][1]+=1ll*C[i][j]*fac[j]%mod*fac[n-1-j]%mod)%=mod;
for(int j=2;j<=n;j++)g[i][j]=1ll*g[i][j-1]*g[i][1]%mod;
}
for(int i=1;i<=n;i++)scanf("%d",&p[i]);
for(int i=1;i<=n;i++)if(!vis[i]){
int s=0;
for(int j=i;!vis[j];j=p[j])s++,vis[j]=true;
c[s]++;
}
for(int i=1;i<=n;i++)m*=c[i]+1;
for(int i=0;i<m;i++)for(int j=i,k=1;k<=n;k++)sta[i][k]=j%(c[k]+1),j/=c[k]+1;
// for(int i=0;i<m;i++,puts(""))for(int j=1;j<=n;j++)printf("%d ",sta[i][j]);
f[0]=1;for(int i=1;i<m;i++)for(int j=1;j<=i;j++){
int si=0,sj=0,o=0,num=f[i-j];
for(int k=1;k<=n;k++){
num=1ll*num*C[sta[i][k]][sta[j][k]]%mod;
si+=sta[i][k]*k,sj+=sta[j][k]*k,o+=sta[j][k];
}
if(!num)continue;
// printf("%d->%d:%d,%d:%d\n",j,i,sj,si,g[si-sj][sj]);
num=1ll*num*g[si-sj][sj]%mod;
if(!(o&1))num=mod-num;
(f[i]+=num)%=mod;
}
printf("%d\n",f[m-1]);
return 0;
}
IV.图上 DP
图上 DP 常常要运用线代的知识才能解决。
I.CF24D Broken robot
DP必须要有方向性。没有明确顺序的DP都是在耍流氓。这就是为什么有“树上DP”和“DAG上DP”而没有“图上DP”,图上有环就不知道应该按什么顺序做了!(像是基环树DP和仙人掌DP都是缩点了,因此顺序还是确定的;环形DP也有“断环成链”的trick)。
那如果真有DP来给你耍流氓怎么办?
还能怎么办?耍回去啊!
例如这题,有两种思路。
- 同一行中,转移顺序不定;但是不同行之间,转移顺序还是确定的。因此我们行与行之间以普通的DP转移;同一行中,暴力高斯消元消过去。
我们看一下怎么高斯消元。设有行列。
则有
处理一下:
这其中,上一行的DP值可以看作是常量。
这样复杂度是,铁定过不去。
但如果我们把高斯消元的矩阵列出来:
更大一点:
也就是说,它是一个非常稀疏的矩阵,并且非零元素只分布在主对角线两侧!
在这种特殊矩阵上高斯消元只需要消对角线两侧的位置即可,复杂度是的。
则总复杂度是的。
另外,从点出发走到第行,可以看作是从第行的任何点出发,走到点的方案数。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,X,Y;
double f[1010],g[1010][1010];
void Giaos(){
// for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%lf ",g[i][j]);puts("");}puts("");
for(int i=1;i<n;i++){
/*int mp=i;
for(int j=i+1;j<=min(n,i+2);j++)if(fabs(g[j][i])>fabs(g[mp][i]))mp=j;
if(mp!=i){
for(int j=i;j<=min(n,i+2);j++)swap(g[mp][j],g[i][j]);
swap(g[mp][n+1],g[i][n+1]);
}
assert(mp==i);*/
double tmp=g[i+1][i]/g[i][i];
g[i+1][i]=0,g[i+1][i+1]-=tmp*g[i][i+1],g[i+1][n+1]-=tmp*g[i][n+1];
}
// for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%lf ",g[i][j]);puts("");}puts("");
f[n]=g[n][n+1]/g[n][n];
for(int i=n-1;i>=1;i--)f[i]=(g[i][n+1]-g[i][i+1]*f[i+1])/g[i][i];
}
int main(){
scanf("%d%d%d%d",&m,&n,&X,&Y),m-=X-1,X=1;
if(m==1){puts("0");return 0;}
if(n==1){printf("%d\n",(m-1)*2);return 0;}
for(int i=1;i<m;i++){
g[1][1]=2,g[1][2]=-1,g[1][n+1]=f[1]+3;
g[n][n]=2,g[n][n-1]=-1,g[n][n+1]=f[n]+3;
for(int j=2;j<n;j++)g[j][j-1]=g[j][j+1]=-1,g[j][j]=3,g[j][n+1]=f[j]+4;
Giaos();
}
printf("%lf\n",f[Y]);
return 0;
}
- 因为“保留4位小数”,所以……
跑遍最普通的DP完事。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,X,Y;
double f[1010][1010];
int main(){
scanf("%d%d%d%d",&m,&n,&X,&Y),m-=X-1,X=1;
if(m==1){puts("0");return 0;}
if(n==1){printf("%d\n",(m-1)*2);return 0;}
for(int i=1;i<m;i++)for(int tmp=1;tmp<=50;tmp++)for(int j=1;j<=n;j++){
if(j==1)f[i][j]=(f[i][j+1]+f[i][j]+f[i-1][j])/3+1;
else if(j==n)f[i][j]=(f[i][j-1]+f[i][j]+f[i-1][j])/3+1;
else f[i][j]=(f[i-1][j]+f[i][j]+f[i][j-1]+f[i][j+1])/4+1;
}
printf("%lf\n",f[m-1][Y]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?