树的杂记
某次模拟赛的T2 强迫症
题目大意:判断一棵树是否对称。
思路:首先,一棵树的重心一定在对称轴上。一棵树是否对称有两种情况:有点在对称轴上和没有点在对称轴上。没有点在对称轴上的情况一定有两个重心(但有两个重心并不一定没有点在对称轴上)。有两个重心时,我们只需要算出两个重心对应子树的hash值,然后比较,如果相等就对称,否则就选其中一个重心(可以任选,并不影响结果)按一个重心的情况做一遍;有一个重心【可以有两棵单独的自己对称的树】(或上述再做一遍的情况【只能有一棵单独的自己对称的树】)时,我们计算出这个重心所有子树的hash值,放到map里,计算出hash值对应为奇数的hash值个数,如果超过了能单独自己对称树的个数就不对称,否则就判断这棵树能否自己对称【同样只能有一棵单独的自己对称的树】,递归下去。
考试的时候,穷举了根,就会忽略没有点在对称轴上的情况,同时没有想到很好的hash,所以就导致了分数不高。
这道题目的hash的选择要十分小心,很多基于子树的hash如果拆开后可能会出现bug。所以在这道题中应用了位运算(异或是没有分配律的,所以可以避免很多问题)。
#include<iostream> #include<cstdio> #include<cstring> #include<map> #include<cstdlib> #include<ctime> #define maxnode 10005 #define pp 233 using namespace std; int point[maxnode]={0},next[maxnode*2]={0},en[maxnode*2]={0},tot=0, root1,root2,maxsiz,n,siz[maxnode]={0},ha[30]={0}; char color[maxnode]={0}; map <unsigned long long,int> cnt,dui; void add(int u,int v) { ++tot;next[tot]=point[u];point[u]=tot;en[tot]=v; ++tot;next[tot]=point[v];point[v]=tot;en[tot]=u; } void getroot(int u,int fa) { int i,j,mm=0; siz[u]=1; for (i=point[u];i;i=next[i]) if ((j=en[i])!=fa) { getroot(j,u);siz[u]+=siz[j];mm=max(mm,siz[j]); } mm=max(mm,n-siz[u]); if (mm<=maxsiz) { if (mm<maxsiz){maxsiz=mm;root1=u;root2=0;} else root2=u; } } unsigned long long gethash(int u,int fa) { int i,j; unsigned long long ss=0; for (i=point[u];i;i=next[i]) if ((j=en[i])!=fa) ss+=gethash(j,u); return (ss*pp)^(ha[color[u]-'A']); } bool judge(int r1,int r2,int fa,int cn) { int i,j,x,y,cc=0,zhan[3]={0}; unsigned long long ss,s1,s2; if (r2==0) { cnt.clear();dui.clear(); for (i=point[r1];i;i=next[i]) { if ((j=en[i])==fa) continue; ss=gethash(j,r1); if (!cnt.count(ss)) cnt[ss]=0; ++cnt[ss];dui[ss]=j; } map<unsigned long long,int>::iterator it; for (it=cnt.begin();it!=cnt.end();++it) if (it->second %2==1) { ++cc;if (cc<=cn) zhan[cc]=dui[it->first]; } if (cc>cn) return false; for (i=1;i<=cc;++i) if (!judge(zhan[i],0,r1,1)) return false; return true; } else { s1=gethash(r1,r2);s2=gethash(r2,r1); if (s1==s2) return true; return (judge(r1,0,0,2)); } } int main() { freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); int t,i,j,u,v; srand(time(0)); for (i=0;i<26;++i) ha[i]=rand(); scanf("%d",&t); while(t) { scanf("%d",&n);tot=0; memset(point,0,sizeof(point)); memset(next,0,sizeof(next)); for (i=1;i<=n;++i) while(scanf("%c",&color[i])==1) if (color[i]>='A'&&color[i]<='Z') break; for (i=1;i<n;++i) { scanf("%d%d",&u,&v);add(u,v); } maxsiz=n;getroot(1,root1=root2=0); if (judge(root1,root2,0,2)) printf("SYMMETRIC\n"); else printf("NOT SYMMETRIC\n"); --t; } fclose(stdin); fclose(stdout); }
xjoi模拟赛 C
题目大意:给定一棵树,叶子节点是苹果,每个苹果只能在一个时间范围内采摘,一枝上的苹果如果时间有交集可以一起采摘,问最少的次数。
思路:对于一个点的所有儿子,选出最靠右的左端点,那么比这个左端点小的右端点都要单独摘,从这个左端点到它右面第一个右端点之间的时间是这个点可采摘范围,传给它的父亲去决策。最后到根的时候看看如果没摘完就+1。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 1000005 using namespace std; int fi[2][maxm],sz=0,point[maxm]={0},next[maxm]={0},ans=0; void build(int fa){ int i,j,t,u;scanf("%d",&t); next[++sz]=point[fa];point[fa]=sz; if (!t){scanf("%d%d",&fi[0][sz],&fi[1][sz]);return;} for (u=sz,i=1;i<=t;++i) build(u); for (j=0,i=point[u];i;i=next[i]){ ++j;fi[0][u]=max(fi[0][i],fi[0][u]); }for (i=point[u];i;i=next[i]){ if (fi[1][i]<fi[0][u]) ++ans; else fi[1][u]=min(fi[1][u],fi[1][i]); }if (!fa&&fi[1][u]>=fi[0][u]&&fi[1][u]<2100000000) ++ans; } int main(){ memset(fi[0],128,sizeof(fi[0])); memset(fi[1],127,sizeof(fi[1])); build(0);printf("%d\n",ans); }
bzoj2657 旅游
题目大意:给定一个n多边形,n-2个城市都是三角形,且组成了多边形的一种三角剖分,求最多能经过多少个城市(只能走顶点,且顶点不能相邻,可以重复走)。
思路:一个城市一定有一条边不是相邻点间的边(n=3时除外),且一条不是相邻点的边两边各有一个城市,对于有相邻边的三角形连边,一定是一棵树(凸多边形的三角剖分不会在内部在分出一个点来),所以树的直径就是答案。考虑这个直径个数是一定可取的,因为可以在原图中的边来切割树边(对偶图);这个答案也是最大的,因为如果有更优的答案,也就是说从某一个城市可以在到一个城市,那么就在树上有一条可以添入直径的边。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<map> #define maxm 200005 #define maxe 400005 using namespace std; struct use{ int x,y; bool operator<(const use&xi)const{ return (x==xi.x ? y<xi.y : x<xi.x); } }; int point[maxm]={0},en[maxe]={0},next[maxe]={0},ai[maxm][4],tot=0,fi[maxm]={0},ans=0; map <use,int>cnt; void add(int u,int v){ if (!u||!v) return; next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u; } void dfs(int u,int fa){ int i,j,v;fi[u]=1; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dfs(v,u);ans=max(ans,fi[v]+fi[u]); fi[u]=max(fi[u],fi[v]+1); } } int main(){ int n,i,j;scanf("%d",&n); for (i=1;i<=n-2;++i){ for (j=1;j<=3;++j) scanf("%d",&ai[i][j]); sort(ai[i]+1,ai[i]+4); add(i,cnt[(use){ai[i][1],ai[i][2]}]); add(i,cnt[(use){ai[i][1],ai[i][3]}]); add(i,cnt[(use){ai[i][2],ai[i][3]}]); cnt[(use){ai[i][1],ai[i][2]}]=i; cnt[(use){ai[i][1],ai[i][3]}]=i; cnt[(use){ai[i][2],ai[i][3]}]=i; }dfs(1,0);printf("%d\n",ans); }
bzoj2282 消防&&bzoj1999 树网的核
题目大意:给定一棵树,求所有点到一段和不超过s的树链的距离(距离的定义为点到链上点的最短距离)最大值的最小。
思路:这段树链一定在某一条带权直径上(在直径上是因为那样到所有点的距离最小,任意一条直径是因为每一条直径上对应点到另一条直径的距离都相等)。暴力可以枚举链上的起点,然后二分出终点,利用之前dfs出来的所有点到链的距离(从链上每一个点向不再链上的点dfs,每个点只访问一次)算出答案,这样是n^2的。然后发现起点其实有三分性质(左端点向右走的时候,左边是不减的;右端点向右走的时候,右边是不增的;而位于链上的点可以同时看作右边的)。
一开始三分完了之后每次都要对链上的点dfs求距离,非常慢,后来统一求了一次距离保存在链上更新过来的点上,对左边、中间、右边的点枚举求出最大值+相应的值就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<ctime> #define maxm 500005 #define maxe 1000005 using namespace std; int point[maxm]={0},next[maxe]={0},en[maxe]={0},va[maxe]={0},n,s,tot=0,fi[maxm]={0}, ne[maxm][2]={0},len=0,ansi=0,root=0,bi[maxm]={0},dd[maxm]={0},dis[maxm]={0}; bool visit[maxm]={false},li[maxm]={false}; int in(){ int x=0;char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x; } void add(int u,int v,int vv){ next[++tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=vv; next[++tot]=point[v];point[v]=tot;en[tot]=u;va[tot]=vv; } void dfs(int u,int fa){ int i,v,maxn=0,cmaxn=0; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue;dfs(v,u); if (fi[v]+va[i]>maxn){ cmaxn=maxn;ne[u][1]=ne[u][0]; maxn=fi[v]+va[i];ne[u][0]=v; }else if (fi[v]+va[i]>cmaxn){cmaxn=fi[v]+va[i];ne[u][1]=v;} }if (cmaxn+maxn>len){len=cmaxn+maxn;ansi=u;} fi[u]=maxn; } void change(int u){ if (ne[u][0]) {li[ne[u][0]]=true;change(ne[u][0]);} else root=u; if (u==ansi&&ne[u][1]) {li[ne[u][1]]=true;change(ne[u][1]);} } void dfs2(int u,int fa){ int i,v;bi[++bi[0]]=u; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; if (li[v]){dd[bi[0]+1]=dd[bi[0]]+va[i];dfs2(v,u);} } } void dfs3(int u,int anc,int dep){ int i,v;visit[u]=true; if (dep>dis[anc]) dis[anc]=dep; for (i=point[u];i;i=next[i]) if (!visit[v=en[i]]) dfs3(v,anc,dep+va[i]); } int calc(int x){ int i,j=0,ans=0; j=upper_bound(dd+1,dd+bi[0]+1,dd[x]+s)-dd-1; for (ans=0,i=1;i<x;++i) ans=max(ans,dis[i]+dd[x]-dd[i]); for (i=x;i<=j;++i) ans=max(ans,dis[i]); for (i=j+1;i<=bi[0];++i) ans=max(ans,dis[i]+dd[i]-dd[j]); return ans; } int work(){ int l,r,m1,m2;l=1;r=bi[0]; while(l<r){ if (r-l>=3){m1=l+(r-l)/3;m2=r-(r-l)/3;} else{return min(calc(l),min(calc(r),(l<r-1 ? calc(l+1) : 0)));} if (calc(m1)<calc(m2)) r=m2; else l=m1; }return calc(l); } int main(){ int i,u,v,vv;n=in();s=in(); for (i=1;i<n;++i){u=in();v=in();vv=in();add(u,v,vv);} dfs(1,0);li[ansi]=true;change(ansi);dfs2(root,0); for (i=1;i<=bi[0];++i) visit[bi[i]]=true; for (i=1;i<=bi[0];++i) dfs3(bi[i],i,0); printf("%d\n",work()); }
bzoj3124 直径
题目大意:给定一棵树,求树的直径长度和出现在所有直径上的边的条数。
思路:一棵树的所有直径至少交于一个点,同时所有直径的交一定是连续的部分。所以可以找出一条直径,在直径上判断那些是可行的部分,如果直径上一点i不经过直径上的点到直径外的最远距离和直径的某个端点x一样,则i~x的边都不会出现在所有直径上。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 200005 #define M 400005 #define LL long long using namespace std; int point[N]={0},next[M],en[M],tot=0,zh[N],fb[N]={0},pre[N]; LL va[M],len,dis[N],dd[N]; void add(int u,int v,LL vv){ next[++tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=vv; next[++tot]=point[v];point[v]=tot;en[tot]=u;va[tot]=vv;} void gdis(int u,int fa,int k){ int i,v;pre[u]=fa; if (dis[u]>len){ len=dis[u]; if (!k) zh[zh[0]=1]=u; }for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa||fb[v]) continue; dis[v]=dis[u]+va[i];gdis(v,u,k); } } int main(){ int n,i,u,v,l,r;LL vv,ml;scanf("%d",&n); for (i=1;i<n;++i){scanf("%d%d%I64d",&u,&v,&vv);add(u,v,vv);} len=dis[1]=0LL;gdis(1,0,0); len=dis[zh[1]]=0LL;gdis(zh[1],0,0); for (i=1;pre[zh[i]];++i) zh[++zh[0]]=pre[zh[i]]; for (i=1;i<=zh[0];++i){ dd[zh[i]]=dis[zh[i]];fb[zh[i]]=1; }ml=len;printf("%I64d\n",len); l=1;r=zh[0]; for (i=1;i<=zh[0];++i){ len=dis[zh[i]]=0LL;gdis(zh[i],0,1); if (len==ml-dd[zh[i]]) l=i; if (len==dd[zh[i]]){r=i;break;} }printf("%d\n",r-l); }
bzoj3162 独钓寒江雪
题目大意:给定一棵树,求本质不同的独立集的个数。(本质不同指不能通过重新编号使得树的结构相同且选定的点相同)
思路:首先要能找到结构相同的子树,所以可以找到树的重心之后hash,因为重心可能有两个,所以把边拆点,这样重心一定只有一个(可能是点也可能是边)。考虑树型dp,fi[i][0]表示不选i这个点的方案数,fi[i][1]表示选i这个点的方案数,对于不是重心的边i,一定只有一个儿子,所以fi[i]=fi[soni];对于是重心的边i,两个儿子不能同时选,所以是都不选的方案(cc(fi[soni][0],2),相当于一共有n种方案,选m个的可重组合cc(n,m)=c(n+m-1,m))+选一个的(fi[soni][0]*fi[soni][1]);对于结构一样的子树的方案fi[i][1]=∏(结构不同的j)cc(fi[j][0],sizj)(设sizj是i的子树中和j结构一样的子树个数),fi[i][0]=∏(结构不同的j)cc(fi[j][0]+fi[j][1],sizj)。如果重心是边,答案是fi[i][0];如果重心是点,答案是fi[i][0]+fi[i][1]。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 1000005 #define M 2000005 #define p 1000000007LL #define pp 233LL #define LL long long #define UL unsigned long long using namespace std; int point[N]={0},next[M],en[M],tot=0,rt,mn,siz[N]={0},n,zh[N]; LL fi[N][2],ci[N]; UL hv[N],rv[N]; void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u;} void find(int u,int fa){ int i,v,mz=0;siz[u]=1; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; find(v,u);siz[u]+=siz[v]; mz=max(mz,siz[v]); }mz=max(mz,n*2-1-siz[u]); if (mz<mn){rt=u;mn=mz;} } void geth(int u,int fa){ int i,v;siz[u]=1;hv[u]=0; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; geth(v=en[i],u);siz[u]+=siz[v]; hv[u]+=hv[v]; }hv[u]=(hv[u]*p)^rv[siz[u]];} int cmp(int x,int y){return hv[x]<hv[y];} LL mi(LL x,int y){ LL a=1LL; for (;y;y>>=1){ if (y&1) a=a*x%p; x=x*x%p; }return a;} LL getc(LL n,LL m){ LL i,ci,ans; ci=ans=1LL;n=n+m-1; for (i=n-m+1;i<=n;++i) ans=ans*i%p; for (i=1LL;i<=m;++i) ci=ci*i%p; return ans*mi(ci,(int)(p-2LL))%p;} void dp(int u,int fa){ int i,j,v;LL cc;fi[u][0]=fi[u][1]=1LL; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dp(v,u); if (u>n&&u!=rt){fi[u][0]=fi[v][0];fi[u][1]=fi[v][1];} }if (u>n&&u!=rt) return; for (zh[0]=0,i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; zh[++zh[0]]=v; }sort(zh+1,zh+zh[0]+1,cmp); for (i=1;i<=zh[0];i=j+1){ j=i;while(j<zh[0]&&hv[zh[j+1]]==hv[zh[j]]) ++j; if (u>n&&u==rt) fi[u][0]=fi[u][0]*(getc(fi[zh[i]][0],(LL)(j-i+1))+fi[zh[i]][0]*fi[zh[i]][1]%p)%p; else{ fi[u][0]=fi[u][0]*getc(fi[zh[i]][0]+fi[zh[i]][1],(LL)(j-i+1))%p; fi[u][1]=fi[u][1]*getc(fi[zh[i]][0],(LL)(j-i+1))%p; } } } int main(){ int i,j,u,v;scanf("%d",&n); for (i=1;i<=n;++i) rv[i]=(UL)rand(); for (i=1;i<n;++i){ scanf("%d%d",&u,&v); add(u,i+n);add(v,i+n); }mn=2*n-1;find(1,0);geth(rt,0);dp(rt,0); printf("%I64d\n",(rt>n ? fi[rt][0] : fi[rt][0]+fi[rt][1])%p); }
bzoj4424 Fairy
题目大意:给定一张无向图,问删掉那些边能使原图是二分图(每次只删掉这一条边)。
思路:有奇环的时候就不是二分图。先dfs出一棵树(这样保证了非树边都是返祖边),二分图染色。1)如果只有树边所有边都可以删;2)如果只有一条非树边,这条边可以删;3)有非树边的时候,只由那些被所有能构成奇环的非树边经过且不被能构成偶环的树边可以删掉。统计每条边被什么边经过可以在树上查分,前缀和一下,然后判断(注意重边自环的处理)。
有点类似bzoj4025,不过这题的要求更少,所以有复杂度更优的算法。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 1000005 #define M 2000005 using namespace std; struct use{int v,id;}ed[M]; int point[N]={0},next[M],tot=0,fi[N]={0},gi[N]={0},dep[N]={0},zh[N],fa[N]={0},co[N]={0}; bool ans[M]={false},vi[N],vv[M]={false}; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} void add(int u,int v,int x){ next[++tot]=point[u];point[u]=tot;ed[tot]=(use){v,x}; next[++tot]=point[v];point[v]=tot;ed[tot]=(use){u,x};} void pre(int u,int ff){ int i,j,v;vi[u]=true;zh[++zh[0]]=u; co[u]=co[ff]^1;dep[u]=dep[ff]+1; for (i=point[u];i;i=next[i]){ if (vi[v=ed[i].v]) continue; fa[v]=i;vv[i]=true;pre(v,u); } } void dfs(int u,int ff){ int i,j,v;vi[u]=true; for (i=point[u];i;i=next[i]){ if (vi[v=ed[i].v]) continue; dfs(v,u);fi[u]+=fi[v];gi[u]+=gi[v]; } } int main(){ int i,j,k,n,m,u,v,cnt=0,po,cn=0;n=in();m=in(); for (i=1;i<=m;++i){u=in();v=in();add(u,v,i);} for (i=1;i<=n;++i){ if (vi[i]) continue; zh[0]=0;pre(i,0); for (j=1;j<=zh[0];++j){ for (k=point[zh[j]];k;k=next[k]){ if (dep[ed[k].v]==dep[zh[j]]){++cn;po=k;} if (dep[ed[k].v]<=dep[zh[j]]||vv[k]) continue; if (co[ed[k].v]==co[zh[j]]){ ++fi[ed[k].v];--fi[zh[j]];++cnt;po=k; }else{++gi[ed[k].v];--gi[zh[j]];} }vi[zh[j]]=false; }dfs(i,0); }cnt+=cn/2; if (!cnt){ for (i=1;i<=n;++i) for (j=point[i];j;j=next[j]) ans[ed[j].id]=true; }else{ if (cnt==1) ans[ed[po].id]=true; for (i=1;i<=n;++i) if (fi[i]==cnt&&!gi[i]) ans[ed[fa[i]].id]=true; }for (cnt=0,i=1;i<=m;++i) if (ans[i]) ++cnt; printf("%d\n",cnt); for (i=1;i<=m;++i) if (ans[i]) printf("%d ",i); printf("\n"); }
bzoj4238 电压
题目大意:通过改变不同点的颜色,使得去掉一条边之后原图是二分图,问总共有多少条边可以被删。
思路:同上一题,但要把没有奇环的时候那些偶环上的边判掉。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100005 #define M 400005 using namespace std; struct use{int v,id;}ed[M]; int point[N]={0},next[M],tot=0,fi[N]={0},gi[N]={0},dep[N]={0},zh[N],fa[N]={0},co[N]={0}; bool ans[M]={false},vi[N],vv[M]={false}; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} void add(int u,int v,int x){ next[++tot]=point[u];point[u]=tot;ed[tot]=(use){v,x}; next[++tot]=point[v];point[v]=tot;ed[tot]=(use){u,x};} void pre(int u,int ff){ int i,v;vi[u]=true;zh[++zh[0]]=u; co[u]=co[ff]^1;dep[u]=dep[ff]+1; for (i=point[u];i;i=next[i]){ if (vi[v=ed[i].v]) continue; fa[v]=i;vv[i]=true;pre(v,u); } } void dfs(int u){ int i,v;vi[u]=true; for (i=point[u];i;i=next[i]){ if (vi[v=ed[i].v]) continue; dfs(v);fi[u]+=fi[v];gi[u]+=gi[v]; } } int main(){ int i,j,k,n,m,u,v,cnt=0,po,cn=0;n=in();m=in(); for (i=1;i<=m;++i){u=in();v=in();add(u,v,i);} for (i=1;i<=n;++i){ if (vi[i]) continue; zh[0]=0;pre(i,0); for (j=1;j<=zh[0];++j){ for (k=point[zh[j]];k;k=next[k]){ if (dep[ed[k].v]==dep[zh[j]]){++cn;po=k;} if (dep[ed[k].v]<=dep[zh[j]]||vv[k]) continue; if (co[ed[k].v]==co[zh[j]]){ ++fi[ed[k].v];--fi[zh[j]];++cnt;po=k; }else{++gi[ed[k].v];--gi[zh[j]];} }vi[zh[j]]=false; }dfs(i); }cnt+=cn/2; if (!cnt){ for (i=1;i<=n;++i) for (j=point[i];j;j=next[j]) if (dep[ed[j].v]>dep[i]&&!gi[ed[j].v]) ans[ed[j].id]=true; }else{ if (cnt==1) ans[ed[po].id]=true; for (i=1;i<=n;++i) if (fi[i]==cnt&&!gi[i]) ans[ed[fa[i]].id]=true; }for (cnt=0,i=1;i<=m;++i) if (ans[i]) ++cnt; printf("%d\n",cnt); }
虚树
bzoj2286 消耗战
题目大意:给定一棵树,边有代价,每次操作有一些点上有东西,根是1,要求所有有东西的点和根都不能相连,求割掉边的最小代价。
思路:虚树的一道模板题,可以把代价给点,维护点到根的最小值就可以了。建立虚树的时候一直维护最右链,然后不断的操作。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 500005 #define LL long long #define inf 0x7fffffffffffffffLL #define up 30 using namespace std; struct edge{ int st,en,next;LL va; }ai[maxnode]={0},bi[maxnode]={0}; int id[maxnode]={0},ti=0,point[maxnode]={0},tot=0,hi[maxnode]={0},fa[maxnode][up], dep[maxnode]={0},zh[maxnode]={0}; LL fi[maxnode]={0},rd[maxnode]={0}; int cmp(int x,int y){return id[x]<id[y];} void add(int u,int v,LL w) { ai[++tot].next=point[u];point[u]=tot; ai[tot].en=v;ai[tot].va=w; ai[++tot].next=point[v];point[v]=tot; ai[tot].en=u;ai[tot].va=w; } void ad(int u,int v) { if (u==v) return;bi[++tot].next=point[u]; point[u]=tot;bi[tot].en=v; } void dfs(int u,int f) { int i,j; id[u]=++ti;dep[u]=dep[f]+1;fa[u][0]=f; for (i=1;i<up;++i) fa[u][i]=fa[fa[u][i-1]][i-1]; for (i=point[u];i;i=ai[i].next) if ((j=ai[i].en)!=f){ rd[j]=min(ai[i].va,rd[u]);dfs(j,u); } } int lca(int x,int y) { int i,j;if (dep[x]<dep[y]) swap(x,y); for (i=up-1;i>=0;--i) if (dep[fa[x][i]]>=dep[y]) x=fa[x][i]; if (x==y) return x; for (i=up-1;i>=0;--i) if (fa[x][i]!=fa[y][i]) { x=fa[x][i];y=fa[y][i]; } return fa[x][0]; } void dp(int u) { int i,j;LL sum=0; for (i=point[u];i;i=bi[i].next){ dp(j=bi[i].en);sum+=fi[j]; }point[u]=0; if (sum==0) fi[u]=rd[u]; else fi[u]=min(rd[u],sum); } int main() { int n,i,j,u,v,m,t,top=0,k;LL w;scanf("%d",&n); for (i=1;i<n;++i){scanf("%d%d%I64d",&u,&v,&w);add(u,v,w);} rd[1]=inf;dfs(1,0);scanf("%d",&m); memset(point,0,sizeof(point)); while(m--){ scanf("%d",&k);zh[top=1]=1;tot=0; for (i=1;i<=k;++i) scanf("%d",&hi[i]); sort(hi+1,hi+k+1,cmp);hi[t=1]=hi[1]; for (i=2;i<=k;++i) if (lca(hi[t],hi[i])!=hi[t]) hi[++t]=hi[i]; for (i=1;i<=t;++i){ u=lca(hi[i],zh[top]); while(1){ if (dep[u]>=dep[zh[top-1]]){ ad(u,zh[top--]); if (zh[top]!=u) zh[++top]=u; break; } ad(zh[top-1],zh[top]);--top; } if (hi[i]!=zh[top]) zh[++top]=hi[i]; } for (i=top;i>1;--i) ad(zh[i-1],zh[i]); dp(1);printf("%I64d\n",fi[1]); } }
bzoj3611 大工程
题目大意:给定一棵树,边权为1。有多次操作求某些点两两距离的总和、最大值、最小值。
思路:建出虚树之后,dp更新一下。(注意数组不能memset,用多少清多少;注意long long)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 2000005 #define up 20 #define LL long long #define inf 0x7fffffffffffffffLL using namespace std; int point[maxm]={0},next[maxm]={0},en[maxm],point1[maxm]={0},next1[maxm]={0},en1[maxm],tot=0, fa[maxm][up+1]={0},zhan[maxm]={0},que[maxm]={0},id[maxm]={0},tt=0; LL ans,maxn,minn,siz[maxm]={0},dep[maxm]={0},sum[maxm],mal[maxm],mil[maxm]; bool visit[maxm]={false}; int cmp(int x,int y){return id[x]<id[y];} void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u; } void add2(int u,int v){ if (u==v) return; next1[++tot]=point1[u];point1[u]=tot;en1[tot]=v; } void pre(int u,int ff){ int i,j,v;fa[u][0]=ff;dep[u]=dep[ff]+1LL;id[u]=++tt; for (i=1;i<=up;++i) fa[u][i]=fa[fa[u][i-1]][i-1]; for (i=point[u];i;i=next[i]) if (!dep[v=en[i]]) pre(v,u); } int lca(int u,int v){ int i,j;if (dep[u]<dep[v]) swap(u,v); for (i=up;i>=0;--i) if (dep[fa[u][i]]>=dep[v]) u=fa[u][i]; if (u==v) return u; for (i=up;i>=0;--i) if (fa[u][i]!=fa[v][i]){ u=fa[u][i];v=fa[v][i]; }return fa[u][0]; } void dp(int u){ LL maxl=0,minl;int i,j,v;minl=inf/3; sum[u]=mal[u]=0;mil[u]=(visit[u]?0:inf)/3;siz[u]=(visit[u]?1:0); for (i=point1[u];i;i=next1[i]){ dp(v=en1[i]);ans+=sum[u]*siz[v]+(sum[v]+(dep[v]-dep[u])*siz[v])*siz[u]; siz[u]+=siz[v];sum[u]+=sum[v]+(dep[v]-dep[u])*siz[v]; maxl=max(maxl,mal[u]+dep[v]-dep[u]+mal[v]); mal[u]=max(mal[u],dep[v]-dep[u]+mal[v]); minl=min(minl,mil[u]+dep[v]-dep[u]+mil[v]); mil[u]=min(mil[u],dep[v]-dep[u]+mil[v]); }maxn=max(maxn,maxl);minn=min(minn,minl); point1[u]=0;visit[u]=false; } int main(){ int n,i,j,u,v,q,k,top,tail;scanf("%d",&n); for (i=1;i<n;++i){ scanf("%d%d",&u,&v);add(u,v); }pre(1,0);scanf("%d",&q); for (i=1;i<=q;++i){ for (j=1;j<=tot;++j) next1[j]=0; scanf("%d",&k);v=tot=0; for (j=1;j<=k;++j){ scanf("%d",&que[j]);visit[que[j]]=true; v=(!v ? que[j] : lca(v,que[j])); }sort(que+1,que+k+1,cmp);zhan[top=1]=v; for (j=1;j<=k;++j){ u=lca(zhan[top],que[j]); while(1){ if (dep[u]>=dep[zhan[top-1]]){ add2(u,zhan[top--]); if (zhan[top]!=u) zhan[++top]=u; break; }add2(zhan[top-1],zhan[top]);--top; }if(que[j]!=zhan[top]) zhan[++top]=que[j]; }for (top;top>1;--top) add2(zhan[top-1],zhan[top]); ans=maxn=0;minn=inf;dp(v); printf("%I64d %I64d %I64d\n",ans,minn,maxn); } }
bzoj3572 世界树(!!!)
题目大意:给定一棵树,每次询问设置一些特殊点,其他点选择最近的(距离一样就选编号小的)特殊点,问每个特殊点被选择的数量(包括自身)。
思路:建立虚树,然后两遍dp出虚树上每个点选择的特殊点(一次从下到上,一次从上到下),这些特殊点外侧的点的个数都可以算出来。然后要考虑虚树上一条边所代表的那些点以及他们不在虚树上的子树,可以用倍增的方法,二分出一条链上的mid,然后mid之上的更新给上面,之下的给下面。
注意:1)式子;2)向下dp的时候不仅考虑父亲,还要考虑考虑兄弟的更新。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 600005 #define M 600005 #define inf 1000000000 #define uu 20 using namespace std; int fa[N][uu]={0},dep[N]={0},dt[N]={0},tt=0,siz[N],point[N]={0},next[M],en[M],tot, po1[N]={0},ne1[N],en1[N],que[N],top,zh[N],fi[N]={0},ans[N]={0},hi[N]; bool vi[N]={false}; int cmp(int x,int y){return dt[x]<dt[y];} void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u;} void add1(int u,int v){ if (u==v) return; ne1[++tot]=po1[u];po1[u]=tot;en1[tot]=v;} void pre(int u,int ff){ int i,v;dt[u]=++tt;siz[u]=1; dep[u]=dep[ff]+1;fa[u][0]=ff; for (i=1;i<uu;++i) fa[u][i]=fa[fa[u][i-1]][i-1]; for (i=point[u];i;i=next[i]){ if ((v=en[i])==ff) continue; pre(v,u);siz[u]+=siz[v]; } } int lca(int u,int v){ int i; if (dep[u]<dep[v]) swap(u,v); for (i=uu-1;i>=0;--i) if (dep[fa[u][i]]>=dep[v]) u=fa[u][i]; if (u==v) return u; for (i=uu-1;i>=0;--i) if (fa[u][i]!=fa[v][i]){ u=fa[u][i];v=fa[v][i]; }return fa[u][0]; } int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];} void up(int u,int ff){ int i,v,d1,d2; for (i=po1[u];i;i=ne1[i]){ if ((v=en1[i])==ff) continue; up(v,u); d1=dis(fi[v],u); if (fi[u]) d2=dis(fi[u],u); if ((!fi[u])||(d1<d2)||(d1==d2&&fi[v]<fi[u])) fi[u]=fi[v]; }if (vi[u]) fi[u]=u; } void down(int u,int ff){ int i,v,d1,d2; for (i=po1[u];i;i=ne1[i]){ if ((v=en1[i])==ff) continue; d1=dis(fi[u],v);d2=dis(fi[v],v); if ((d1<d2)||(d1==d2&&fi[u]<fi[v])) fi[v]=fi[u]; down(v,u);} ans[fi[u]]+=siz[u]; if (ff){ int ci,d1,d2,us; for (us=u,i=uu-1;i>=0;--i) if (dep[fa[us][i]]>dep[ff]) us=fa[us][i]; ans[fi[ff]]-=siz[us]; for (ci=u,i=uu-1;i>=0;--i){ if (dep[fa[ci][i]]<dep[ff]) continue; d1=dis(fi[u],fa[ci][i]); d2=dis(fi[ff],fa[ci][i]); if ((d1<d2)||(d1==d2&&fi[u]<fi[ff])) ci=fa[ci][i]; }ans[fi[u]]+=siz[ci]-siz[u]; ans[fi[ff]]+=siz[us]-siz[ci]; }po1[u]=fi[u]=0; } int main(){ int n,m,q,i,j,u,v;scanf("%d",&n); for (tot=0,i=1;i<n;++i){scanf("%d%d",&u,&v);add(u,v);} scanf("%d",&q);pre(1,0); for (i=1;i<=q;++i){ scanf("%d",&m);v=tot=0; for (j=1;j<=m;++j){ scanf("%d",&que[j]); hi[j]=que[j];vi[que[j]]=true; v=(!v ? que[j] : lca(v,que[j])); }sort(que+1,que+m+1,cmp);zh[top=1]=v; for (j=1;j<=m;++j){ u=lca(zh[top],que[j]); while(1){ if (dep[u]>=dep[zh[top-1]]){ add1(u,zh[top--]); if (zh[top]!=u) zh[++top]=u; break; }add1(zh[top-1],zh[top]);--top; }if (que[j]!=zh[top]) zh[++top]=que[j]; }for (;top>1;--top) add1(zh[top-1],zh[top]); if (v!=1) add1(1,v);v=1; up(v,0);down(v,0); for (j=1;j<=m;++j){ printf("%d ",ans[hi[j]]); ans[hi[j]]=0;vi[hi[j]]=false; }printf("\n"); } }
树型dp
CODEVS1380 没有上司的舞会
思路:树形dp。二维数组k[i][0](表示不取i结点),k[i][1](表示取i点)。用了美丽的next数组保存边之间的关系。。。希望提高了效率。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int r[6001]={0},f[6001][2]={0},fa[6001]={0},next[6001]={0},point[6001]={0},ans; void work(int x) { int i,p; p=point[x]; if (p==0) { f[x][0]=0; f[x][1]=r[x]; if (f[x][1]>ans) ans=f[x][1]; return; } while (p!=0) { work(p); f[x][0]=max(f[x][0],f[p][1]+f[x][0]); f[x][1]=max(f[x][1],f[p][0]+f[x][1]); p=next[p]; } if (r[x]>0) f[x][1]=f[x][1]+r[x]; if (f[x][0]>ans) ans=f[x][0]; if (f[x][1]>ans) ans=f[x][1]; } int main() { int n,i,j,l,k; cin>>n; ans=-2100000000; for (i=1;i<=n;++i) cin>>r[i]; for (i=1;i<n;++i) { cin>>l>>k; fa[l]=k; next[l]=point[k]; point[k]=l; } cin>>l>>k; for (i=1;i<=n;++i) if (fa[i]==0) { work(i); cout<<ans<<endl; break; } }
CODEVS1163访问艺术馆
皮尔是一个出了名的盗画者,他经过数月的精心准备,打算到艺术馆盗画。艺术馆的结构,每条走廊要么分叉为二条走廊,要么通向一个展览室。皮尔知道每个展室里藏画的数量,并且他精确地测量了通过每条走廊的时间,由于经验老道,他拿下一副画需要5秒的时间。你的任务是设计一个程序,计算在警察赶来之前(警察到达时皮尔回到了入口也算),他最多能偷到多少幅画。
思路:不一定要取完一个画室中的画,取一幅画要5s。先用递归建树,保存一下父节点,儿子节点,到父亲的距离,自己末端的画数(有的变量可能用不到)。然后开始树状dp,f[i][j]表示第i个节点取j幅画,然后对左右子树进行选择。一点点的算出来。
注意:每次循环就是从0-该子树最多的画数(用一个数组保存一下);work时,从0开始递归,因为input时把1的父节点默认为了0。
#include<cstdio> #include<iostream> #include<cstring> using namespace std; struct use{ int fa,d,va,l,r; }a[1000]; int s,n=0,su[500]={0},ans=0; long long f[500][10000]={0}; void input(int faa) { int x,y,t; cin>>x>>y; ++n;t=n; if (a[faa].l==0) a[faa].l=n; else a[faa].r=n; a[n].fa=faa; a[n].d=x*2; if (y==0) { input(t); input(t); } else a[t].va=y; } void work(int i) { int j,k,l1,l2; if (a[i].l==0) { su[i]=a[i].va; for (j=1;j<=a[i].va;++j) f[i][j]=5*j; return; } work(a[i].l); if (i!=0) work(a[i].r); f[i][0]=0; for (k=1;k<=su[a[i].r];++k) f[i][k]=min(f[i][k],f[a[i].r][k]+a[a[i].r].d); for (j=1;j<=su[a[i].l];++j) f[i][j]=min(f[i][j],f[a[i].l][j]+a[a[i].l].d); for (j=1;j<=su[a[i].l];++j) for (k=1;k<=su[a[i].r];++k) f[i][j+k]=min(f[i][j+k],f[a[i].l][j]+a[a[i].l].d+a[a[i].r].d+f[a[i].r][k]); su[i]=su[a[i].l]+su[a[i].r]; } int main() { int i,j; cin>>s; memset(a,0,sizeof(a)); memset(f,127,sizeof(f)); input(0); work(0); for (i=1;i<=su[0];++i) if (f[0][i]<=s&&i>ans) ans=i; cout<<ans<<endl; }
题目描述 Description
设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第j个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
若某个子树为主,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空
子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
(1)tree的最高加分
(2)tree的前序遍历
现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。
思路: 比较简单的树形dp,每次都穷举根节点,然后简单的递归。不过要输出路径,可以用最短路时的思路,保存l、r为左右端点的根节点,然后递归输出。还有就是要判断是否该区间已有值,若有值就一定是最优值,不需要在穷举了。
#include<iostream> #include<cstdio> using namespace std; long long f[50][50]={0}; int a[50]={0},path[50][50]={0}; void print(int l,int r) { if (l>r) return; if (path[l][r]!=0) { printf("%d ",path[l][r]); print(l,path[l][r]-1); print(path[l][r]+1,r); } } void work(int l,int r) { int j; if (l>r) { f[l][r]=1; path[l][r]=0; return; } if (f[l][r]!=0) return; if (l==r) { f[l][r]=a[l]; path[l][r]=l; return; } for (j=l;j<=r;++j) { work(l,j-1); work(j+1,r); if (f[l][j-1]*f[j+1][r]+a[j]>f[l][r]) { f[l][r]=f[l][j-1]*f[j+1][r]+a[j]; path[l][r]=j; } } } int main() { int i,j,n; scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&a[i]); work(1,n); printf("%d\n",f[1][n]); print(1,n); cout<<endl; }
bzoj1017 魔兽地图(!!!)
题目大意:有n中装备,有的是基础装备、有的是高级的,高级的需要其他装备来合成它,且合成关系是一棵树。已知所有装备的价值、基础装备的价格和数量上限、高级装备的合成路线,求最大价值。
思路:树上dp。可以设va[soni]表示soni合成i这个儿子所要的个数,vv[i]表示i这个点的花费,up[i]表示i这个点最多取几个,fi[i][j][k]表示i这个点有j个是给父亲合成用的花费为k的最大价值;gi[i][j]表示前i个儿子花费为j的最大价值,穷举k个来合成父亲,gi[i][j]=max(gi[i-1][ll-l]+fi[soni][k*va[soni]][l]);再回来更新fi[][][],fi[i][j][k]=max(gi[sizi][l]+(ll-l)*st[i])。
注意:1、dp下标要搞清楚;2、把数组清成相加不会炸int的可以用memset(fi,-0x3f3f3f3f,sizeof(fi))。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define inf 2100000000 using namespace std; int m,fi[55][105][2005],point[55]={0},en[10000]={0},next[10000]={0},va[10000]={0},ru[55]={0}, st[55]={0},up[55]={0},vv[55]={0},tot=0,ans=0,gi[55][2005]; void add(int u,int v,int vaa){ next[++tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=vaa;++ru[v]; } void dp(int u){ int i,j,k,v,siz=0,l; for (i=point[u];i;i=next[i]){ v=en[i];dp(v);++siz; vv[u]+=va[i]*vv[v]; }if (!siz){ up[u]=min(up[u],m/vv[u]); for (i=0;i<=up[u];++i) for (j=i;j<=up[u];++j) ans=max(ans,fi[u][i][j*vv[u]]=(j-i)*st[u]); }else{ for (up[u]=inf,i=point[u];i;i=next[i]) up[u]=min(up[u],up[en[i]]/va[i]); up[u]=min(up[u],m/vv[u]); memset(gi,-0x3f3f3f3f,sizeof(gi)); gi[0][0]=0; for (j=up[u];j>=0;--j){ int maxn=0; for (siz=0,i=point[u];i;i=next[i]){ v=en[i];++siz; for (k=0;k<=m;++k) for (l=0;l<=k;++l) gi[siz][k]=max(gi[siz][k],gi[siz-1][k-l]+fi[v][va[i]*j][l]); }for (k=0;k<=j;++k) for (l=0;l<=m;++l){ fi[u][k][l]=max(fi[u][k][l],gi[siz][l]+(j-k)*st[u]); ans=max(ans,fi[u][k][l]); } } } } int main(){ int n,i,j,c,u,v;char ch; scanf("%d%d",&n,&m); for (i=1;i<=n;++i){ scanf("%d%*c%c",&st[i],&ch); if (ch=='A'){ scanf("%d",&c); for (j=1;j<=c;++j){ scanf("%d%d",&u,&v); add(i,u,v); } }else scanf("%d%d",&vv[i],&up[i]); }memset(fi,-0x3f3f3f3f,sizeof(fi)); for (i=1;i<=n;++i) if(!ru[i])dp(i); printf("%d\n",ans); }
bzoj1217 消防局的设立||poj2152 消防站(!!!)
题目大意:给定一棵树,在一些点上建消防站,使得任意一个点都能在di距离内找到一个消防站,求建满足的消防站的代价wi和最小。
思路:bzoj上的题目没有边权同时di=2,所以可以树上dp一下。但是对于poj上的题目就要复杂很多,看了论文之后有所理解。
我们可以设gi[i]表示i这个点的子树都满足要求(同时能满足要求的消防站都在i这个点的子树内),fi[i][j]表示i这个子树内满足要求(必须要选j作为消防站)的最优解。然后考虑dp转移,gi[i]就是所有j是i子树内的点的fi[i][j]最小值;fi[i][j]的转移就是:如果dis[i][j]>di[i],fi[i][j]=inf;如果dis[i][j]<=di[i],那么:如果j是i,fi[i][j]=wi[i]+sigma(k是i的儿子)min(fi[k][j],gi[k]);如果j是i子树中的,fi[i][j]=fi[ch][j](j在ch的子树中,且ch是i的儿子)+sigma(k是i的儿子,且k!=ch)(fi[k][j],gi[k]);如果j在i的子树外面,fi[i][j]=sigma(k是i的儿子)min(fi[k][j],gi[k])。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 2005 #define inf 2100000000 using namespace std; int point[maxm]={0},next[maxm]={0},en[maxm]={0},va[maxm]={0},fi[maxm][maxm]={0},gi[maxm]={0}, dis[maxm][maxm]={0},wi[maxm]={0},di[maxm]={0},tot=0,pre[maxm]={0},las[maxm]={0},n; void add(int u,int v,int vv){ next[++tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=vv; next[++tot]=point[v];point[v]=tot;en[tot]=u;va[tot]=vv; } void dfs(int u,int fa,int kk){ int i,j,v;pre[u]=++tot; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dis[kk][v]=dis[kk][u]+va[i];dfs(v,u,kk); }las[u]=tot; } void dp(int u,int fa){ int i,j,v,ch,sum; for (i=point[u];i;i=next[i]){if ((v=en[i])==fa) continue;dp(v,u);} for (i=1;i<=n;++i){ if (dis[i][u]>di[u]) fi[u][i]=inf; else{ sum=0; for (j=point[u];j;j=next[j]){ if ((v=en[j])==fa) continue; sum+=min(gi[v],fi[v][i]); }if (i==u){fi[u][i]=wi[i]+sum;gi[u]=min(gi[u],fi[u][i]);} else{ if (pre[i]>=pre[u]&&las[i]<=las[u]){ for (ch=point[u];ch;ch=next[ch]){ if ((v=en[ch])==fa) continue; if (pre[i]>=pre[v]&&las[i]<=las[v]) break; }ch=en[ch]; fi[u][i]=fi[ch][i]+sum-min(gi[ch],fi[ch][i]); gi[u]=min(gi[u],fi[u][i]); }else fi[u][i]=sum; } } } } int main(){ int i,j,u,v,l,ans;scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&wi[i]); for (i=1;i<=n;++i) scanf("%d",&di[i]); for (i=1;i<n;++i){scanf("%d%d%d",&u,&v,&l);add(u,v,l);} for (i=n;i>=1;--i){tot=0;dfs(i,0,i);} memset(gi,127,sizeof(gi));dp(1,0); printf("%d\n",gi[1]); }
bzoj4011 落忆枫音
题目大意:给定一个有向无环图,加上一条有向边,求图的生成树的个数(要求以1为根,能到所有点)。
思路:如果无环,就是入度之积。加上一条边后可能有环,所以要在所有答案里减去有环的那些,可以用总的方案*环的概率表示这个环存在的个数,这个概率可以用dp来求(拓扑序进行),从这条形成环的边的终点处才有dp值,因为要除,所以要逆元,拓扑的时候不能考虑那条环边,但是答案里要考虑。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 200005 #define LL long long #define p 1000000007LL using namespace std; int point[maxm]={0},next[maxm]={0},en[maxm]={0},tot=0,que[maxm]={0}; LL rd[maxm]={0LL},fi[maxm]={0LL},ci[maxm],ni[maxm]={0},ans; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;++rd[v];} LL mi(LL x,LL y){ if (y==0) return 1LL; if (y==1) return x%p; LL mm=mi(x,y/2); if (y%2) return mm*mm%p*x%p; else return mm*mm%p; } void dfs(int n,int ui,int vi){ int u,v,i,j,head=0,tail=0; for (i=1;i<=n;++i){ni[i]=mi(rd[i],p-2);ci[i]=rd[i]-(i==vi);} que[++tail]=1; while(head!=tail){ u=que[++head]; if (u==vi) fi[u]=ni[u]; for (i=point[u];i;i=next[i]){ fi[v=en[i]]=(fi[v]+fi[u]*ni[v]%p)%p; if (!(--ci[v])) que[++tail]=v; } }ans=((ans-ans*fi[ui]%p)%p+p)%p; } int main(){ int i,j,n,m,u,v,ui,vi;ans=1LL; scanf("%d%d%d%d",&n,&m,&ui,&vi); for (i=1;i<=m;++i){scanf("%d%d",&u,&v);add(u,v);} for (i=2;i<=n;++i) ans=ans*(rd[i]+=(i==vi))%p; dfs(n,ui,vi); printf("%I64d\n",ans); }
bzoj1065 奥运物流(!!!)
题目大意:每个点有一个父亲、常数ci、k,一个点i的权值vi=c+k*sigma(j是i的儿子)vj,可以至多改变m个点的后继,求根节点1的最大权值。
思路:每个点的贡献是ci*k^(dep-1)*(1+k^len+k^2len...)=ci*k^(dep-1)*(1-k^nlen)/(1-k^len),len表示环的长度,因为k<1,所以可以看作=ci*k^(dep-1)/(1-k^len),所以一个点的dep越小,贡献越大,所以修改一定是把一个点的父亲换为1,树型dp。先枚举环的长度就可以确定树的形态了,然后dfs一遍,更新答案。
注意:做背包的部分的时候,因为新儿子可能选0,所以不能直接用dp的数组,而要单开一个变量保存最优值,然后赋过去。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 65 #define LD double #define inf 2100000000 using namespace std; int point[N]={0},next[N],en[N],si[N],tot=0,dep[N],cl,m,gi[N]={0}; LD ci[N],fi[N][N][N]={0.},mi[N]; bool cir[N]={false}; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} void dfs(int u,int de){ int i;dep[u]=de; for (i=point[u];i;i=next[i]) dfs(en[i],de+1); } void dp(int u,int len,int de){ int i,j,a,b,v; for (i=point[u];i;i=next[i]) dp(en[i],len,de+1); for (i=1;i<=de;++i){ if (i==1&&de>1) continue; if (i!=2&&gi[u]==len) continue; if (gi[u]&&gi[u]<len&&u!=1&&i!=(len-gi[u]+2)) continue; fi[u][i][0]=ci[u]*mi[i-1]/(1.-mi[len]); for (j=point[u];j;j=next[j]){ v=en[j]; for (a=m;a>=0;--a){ if (!a) fi[u][i][a]+=fi[v][i+1][a]; else{ LD mx=-inf; for (b=0;b<=a;++b) mx=max(mx,fi[u][i][a-b]+ max(fi[v][i+1][b],b ? fi[v][2][b-1] : -inf)); fi[u][i][a]=mx; } } } } } int main(){ int n,i,j,a,b,c;LD ans=0.,k; scanf("%d%d%lf",&n,&m,&k); for (i=1;i<=n;++i){ scanf("%d",&si[i]); if (i>1) add(si[i],i); }for (i=1;i<=n;++i) scanf("%lf",&ci[i]); for (mi[0]=1.,i=1;i<N;++i) mi[i]=mi[i-1]*k; dfs(1,1);cir[1]=true;gi[1]=1; for (j=1,i=si[1];i!=1;i=si[i]){ cir[i]=true;gi[i]=++j; }for (i=cl=dep[si[1]];i>=2;--i){ for (a=1;a<=n;++a) for (b=0;b<=n;++b) for (c=0;c<=m;++c) fi[a][b][c]=-inf; dp(1,i,1);ans=max(ans,fi[1][1][m]); }printf("%.2f\n",ans); }
bzoj1063 道路设计
题目大意:给定一个森林结构,可以选择一些不相交的链改建为铁路,定义一个点的不舒服值是从这个点到根节点1的路径上经过的未改建的路数,求不舒服值最大的那个点最小值和方案数。
思路:如果这不是一棵树,就输出-1-1;树的情况下可以dp,fi[i][j][k]表示i这个点以及子树中的点到i的不舒服值最大为j,i和儿子连接状况为k的方案数(0<=k<=2,分别表示不和儿子相连,和一个和两个儿子相连),对于k=2的情况,用之前的k=1和新儿子更新;k=1的用k=0和新儿子更新;k=0的用儿子的1、2、3更新。注意方案数是乘法原理,所以考虑新儿子的时候,如果新儿子不与父亲连接的话,还要给原来的情况中*表示新儿子不与父亲相连的情况。
注意:因为有%q,所以还要处理一下%q==0的情况,可以%的时候(x-1)%q+1,这样存在方案的就不是0了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100005 #define up 20 #define LL long long using namespace std; int point[N]={0},next[N<<1],en[N<<1],tot=0,cnt=0; LL q,fi[N][up][3]={0}; void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u;} void dp(int u,int fa){ int i,j,v;LL a,b;++cnt; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dp(v,u); for (j=0;j<up;++j){ a=fi[v][j][0]+fi[v][j][1]; b=(j ? fi[v][j-1][0]+fi[v][j-1][1]+fi[v][j-1][2] : 0LL); fi[u][j][2]=(fi[u][j][2]*b+fi[u][j][1]*a-1)%q+1; fi[u][j][1]=(fi[u][j][1]*b+fi[u][j][0]*a-1)%q+1; fi[u][j][0]=(fi[u][j][0]*b-1)%q+1; } } } int main(){ int n,m,i,j,u,v; scanf("%d%d%I64d",&n,&m,&q); for (i=1;i<=m;++i){scanf("%d%d",&u,&v);add(u,v);} for (i=1;i<=n;++i) for (j=0;j<up;++j) fi[i][j][0]=1LL; dp(1,0); if (cnt<n) printf("-1\n-1\n"); else{ for (j=0;j<up;++j) if (fi[1][j][0]+fi[1][j][1]+fi[1][j][2]) break; printf("%d\n%I64d\n",j,(fi[1][j][0]+fi[1][j][1]+fi[1][j][2])%q); } }
bzoj4013 实验比较
题目大意:已知一些图片间的关系,形如a<b或者a=b,每个b是不同的,合法序列是包含n个图片的,形如a1</=a2</=a3...,求合法序列的个数。
思路:因为每个b不同,所以是一个森林或者环。如果有环或者两个数既=有<都是0。设fi[i][j]表示i这个点后面序列长为j的方案数,考虑合并两个儿子u、v,枚举u的长度a、v的长度b、合并后的长度k(max(a,b)<=k<=a+b),相当于向k个盒子中放入a个白球和b个黑球,每个盒子每种颜色只能放一个,方案是c(k,a)*c(a,b-k+a),相当于先选a个放白球、剩下的放黑球、再从a个放白球的中间选出b-k+a个放黑球,对于树上的点,要放到j+1的位置。
注意:判断环的时候非常多情况。
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define N 205 #define LL long long #define p 1000000007LL using namespace std; int fa[N],point[N]={0},next[N],en[N],tot=0,zh[N]={0},scc[N]={0},scnt=0,pre[N],low[N],tt=0, po1[N]={0},ne1[N],en1[N],cnt=0,siz[N]={0},du[N]={0},map[N][N]={0}; LL fac[N],inv[N],fi[N][N]={0LL},c[N][N],ans=1LL,gi[N]; bool vi[N]={false},mp[N][N]={false}; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} void add1(int u,int v){ if (mp[u][v]) return;mp[u][v]=true; ne1[++tot]=po1[u];po1[u]=tot;en1[tot]=v;++du[v];} char in(){ char ch=getchar(); while(ch!='='&&ch!='<') ch=getchar(); return ch;} void tarjan(int u){ int i,v;zh[++zh[0]]=u; pre[u]=low[u]=++tt; for (i=point[u];i;i=next[i]){ v=en[i]; if (!pre[v]){tarjan(v);low[u]=min(low[u],low[v]);} else if (!scc[v]) low[u]=min(low[u],pre[v]); }if (pre[u]==low[u]){ ++scnt; while(zh[0]){ scc[v=zh[zh[0]]]=scnt; --zh[0];if (v==u) break; } } } void dfs2(int u,int anc){ int i,v;if (vi[scc[anc]]) return; for (i=point[u];i;i=next[i]){ v=en[i]; if (pre[v]){ if (pre[u]+map[u][v]-1-pre[v]>=1) vi[scc[anc]]=true; continue; }else{ pre[v]=pre[u]+map[u][v]-1; dfs2(v,anc); } } } void dfs(int u){ int i,k,a,b,v; for (i=po1[u];i;i=ne1[i]) dfs(en1[i]); for (i=po1[u];i;i=ne1[i]){ v=en1[i]; if (i==po1[u]){ siz[u]+=siz[v]; for (k=1;k<=siz[u];++k) fi[u][k]=fi[v][k]; }else{ memset(gi,0,sizeof(gi)); for (a=1;a<=siz[u];++a) for (b=1;b<=siz[v];++b) for (k=max(a,b);k<=a+b;++k) gi[k]=(gi[k]+fi[u][a]*fi[v][b]%p*c[k][a]%p*c[a][b-k+a]%p)%p; siz[u]+=siz[v]; for (k=1;k<=siz[u];++k) fi[u][k]=gi[k]; } }if (!po1[u]) fi[u][1]=1LL; else for (i=siz[u]+1;i;--i) fi[u][i]=fi[u][i-1]; ++siz[u];} int main(){ int i,j,n,m,u,v,a,b,k;char ch; scanf("%d%d",&n,&m); for (i=0;i<N;++i) for (c[i][0]=1LL,j=1;j<=i;++j) c[i][j]=(c[i-1][j]+c[i-1][j-1])%p; for (i=1;i<=m;++i){ scanf("%d",&u);ch=in(); scanf("%d",&v);add(u,v); if (ch=='='){ add(v,u); if (map[v][u]>1||map[u][v]>1){printf("0\n");return 0;} map[u][v]=map[v][u]=1; }else{ if (map[u][v]==1){printf("0\n");return 0;} map[u][v]=2; } }for (i=1;i<=n;++i) if (!pre[i]) tarjan(i); for (i=1;i<=n;++i){ memset(pre,0,sizeof(pre)); pre[i]=1;dfs2(i,i); if (vi[scc[i]]){printf("0\n");return 0;} }for (tot=0,i=1;i<=n;++i) for (j=point[i];j;j=next[j]) if (scc[i]!=scc[en[j]]) add1(scc[i],scc[en[j]]); for (j=0,i=1;i<=scnt;++i){ if (du[i]) continue;dfs(i); if (!j){ siz[0]+=siz[i]; for (k=1;k<=siz[0];++k) fi[0][k]=fi[i][k]; }else{ memset(gi,0,sizeof(gi)); for (a=1;a<=siz[0];++a) for (b=1;b<=siz[i];++b) for (k=max(a,b);k<=a+b;++k) gi[k]=(gi[k]+fi[0][a]*fi[i][b]%p*c[k][a]%p*c[a][b-k+a]%p)%p; siz[0]+=siz[i]; for (k=1;k<=siz[0];++k) fi[0][k]=gi[k]; }j=i; }for (ans=0LL,i=1;i<=n;++i) ans=(ans+fi[0][i])%p; printf("%I64d\n",ans); }
bzoj4446 小凸玩密室
题目大意:一棵完全二叉树,有点权和边权,每个点有一个灯,第一灯不花代价,第二个灯v的代价是dis(la,v)*ai[v],每次点的灯必须和之前的联通,问点亮所有灯的最小代价。
思路:fi[i][j]表示i的子树点完,最后点的是j,必须先点i的最小代价;gi[i][j]表示i的子树点完,最后点的是j,可以先点其他的最小代价。因为是完全二叉树,所以状态是nlogn的。枚举左右儿子用最优值更新,也是nlogn的。
注意:因为二叉树可能不满,可以把最后一层补满。这些点的边权值0,但点权不能为0(可能出现本来停在这个子树的其他点,但因为点权是0,所以又过来了的情况,再向上更新的时候有问题),赋成一个相对较大的数(也不能乘爆LL)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 300005 #define up 20 #define LL long long #define inf 0x7fffffffffffffffLL using namespace std; int n,li[N],ri[N]; LL ai[N]={0LL},bi[N]={0LL},dep[N]={0LL},fi[up][N],gi[up][N]; void dp(int u,int dd){ int i,v,ls,rs;LL lf,lg,rf,rg; dep[u]=dep[u>>1]+bi[u-1]; lf=lg=rf=rg=inf/3LL; ls=((u<<1)<=n ? (u<<1) : 0); rs=((u<<1|1)<=n ? (u<<1|1) : 0); if (ls){ dp(ls,dd+1);dp(rs,dd+1); for (i=li[ls];i<=ri[ls];++i){ lf=min(lf,fi[dd+1][i]+(dep[i]-dep[u])*ai[rs]); lg=min(lg,gi[dd+1][i]+(dep[i]-dep[u])*ai[u]); }lf+=(dep[ls]-dep[u])*ai[ls]; for (i=li[rs];i<=ri[rs];++i){ rf=min(rf,fi[dd+1][i]+(dep[i]-dep[u])*ai[ls]); rg=min(rg,gi[dd+1][i]+(dep[i]-dep[u])*ai[u]); }rf+=(dep[rs]-dep[u])*ai[rs]; }else fi[dd][u]=gi[dd][u]=lf=lg=rf=rg=0LL; if (ls){ for (i=li[ls];i<=ri[ls];++i){ fi[dd][i]=min(fi[dd][i],fi[dd+1][i]+(dep[ls]-dep[u])*ai[ls]+rf); gi[dd][i]=min(gi[dd][i],fi[dd+1][i]+(dep[ls]-dep[u])*ai[ls]+rg); }for (i=li[rs];i<=ri[rs];++i){ fi[dd][i]=min(fi[dd][i],fi[dd+1][i]+(dep[rs]-dep[u])*ai[rs]+lf); gi[dd][i]=min(gi[dd][i],fi[dd+1][i]+(dep[rs]-dep[u])*ai[rs]+lg); } }for (i=li[u];i<=ri[u];++i) gi[dd][i]=min(gi[dd][i],fi[dd][i]); } int main(){ int i,j;LL ans;scanf("%d",&n); memset(fi,127/3,sizeof(fi)); memset(gi,127/3,sizeof(gi)); for (i=1;i<N;++i) ai[i]=inf/1000000000LL; for (i=1;i<=n;++i) scanf("%I64d",&ai[i]); for (i=1;i<n;++i) scanf("%I64d",&bi[i]); for (i=1;i<=n;i<<=1);n=i-1; for (i=1;i<=n;++i){ for (li[i]=ri[i]=i;(li[i]<<1)<=n;li[i]=li[i]<<1,ri[i]=ri[i]<<1|1); ri[i]=min(ri[i],n); }dp(1,1);ans=inf; for (i=1;i<=n;++i) ans=min(ans,min(fi[1][i],gi[1][i])); printf("%I64d\n",ans); }
HAOI2015树上染色
题目大意:给树上n个点染k个黑色,求黑点之间、白点之间距离和的最大值。
思路:感觉是树形dp,但是觉得转移的时候很难受。后来听到fye大神的背包,就开始想背包方向想,发现多重背包可以解决。我们dfs一个孩子之后就可以进行背包了。但是赋初值的时候,要给f[u][0]和f[u][1]都赋为0,因为对于u这个根节点,我们可以涂两种颜色,初始值中的f[u][0]表示把它涂成白色,而f[u][1]表示涂成黑色,其他都是-∞就可以了。
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #define maxnode 2001 using namespace std; int point[maxnode]={0},next[maxnode*2]={0},en[maxnode*2]={0},tot=0,siz[maxnode]={0},n,kk; long long va[maxnode*2]={0},f[maxnode][maxnode]={0}; bool visit[maxnode]={false}; void add(int u,int v,long long st) { ++tot;next[tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=st; ++tot;next[tot]=point[v];point[v]=tot;en[tot]=u;va[tot]=st; } void dfs(int u) { int i,j,k,t; long long sum; siz[u]=1;visit[u]=true;f[u][0]=f[u][1]=0; for (i=point[u];i;i=next[i]) if (!visit[j=en[i]]) { dfs(j);siz[u]+=siz[j]; for (k=siz[u];k>=0;--k) for (t=0;t<=siz[j]&&t<=k;++t) { sum=f[u][k-t]+f[j][t]+va[i]*(long long)(t*(kk-t))+va[i]*(long long)((siz[j]-t)*(n-kk-(siz[j]-t))); f[u][k]=max(f[u][k],sum); } } } int main() { int i,j,u,v; long long st; scanf("%d%d",&n,&kk); for (i=1;i<n;++i) { scanf("%d%d%I64d",&u,&v,&st); add(u,v,st); } for (i=0;i<maxnode;++i) for (j=0;j<maxnode;++j) f[i][j]=-999999999999; dfs(1); printf("%I64d\n",f[1][kk]); }
bzoj3677 连珠线
题目大意:每次有两种操作:(1)把一个珠子和一串珠子中的连起来红线;(2)把一个珠子放到红线中间,并用蓝线把珠子和两端连起来。给出一个最终的形状,权值和是蓝线权值和,问最大权值和。
思路:蓝线都是两两相连。fi[i][j]表示i的时候5中状态的连线:0、i有0个不配对的蓝线;1、i有1个不配对的蓝线;2、i有两个配对的蓝线;3、i有0个不配对的篮线且子树中有两个配对的蓝线;4、i有1个不配对的蓝线且子树中有两个配对的蓝线。互相更新。
注意:有一种不符合的情况就是两对蓝线通过中间的点连起来,很多情况都不能更新。(!!!)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 200005 #define LL long long using namespace std; int point[N]={0},next[N<<1],en[N<<1],tot=0; LL va[N<<1],fi[N][5]; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} void add(int u,int v,LL vv){ next[++tot]=point[u];point[u]=tot;en[tot]=v;va[tot]=vv; next[++tot]=point[v];point[v]=tot;en[tot]=u;va[tot]=vv;} void dp(int u,int ff){ int i,v;LL f0,f1,f2,f3,f4,v0,v1,v2,v3,v4; fi[u][0]=0LL; for (i=point[u];i;i=next[i]){ if ((v=en[i])==ff) continue; dp(v,u); v0=fi[v][0];v1=fi[v][1];v2=fi[v][2]; v3=fi[v][3];v4=fi[v][4]; f0=fi[u][0];f1=fi[u][1];f2=fi[u][2]; f3=fi[u][3];f4=fi[u][4]; fi[u][0]=max(f0,max(f0+va[i]+v1,f0+v0)); fi[u][1]=max(f1,max(f0+v0+va[i],f1+max(v0,va[i]+v1))); fi[u][2]=max(f2,max(f1+va[i]+max(v0,max(v2,v3)),max(f2+max(va[i]+v1,v0),f4+va[i]+v0))); fi[u][3]=max(f3,max(f3+max(v0,va[i]+v1),max(f3+v0,f0+max(v2,max(v3,v4+va[i]))))); fi[u][4]=max(f4,max(f0+max(v2,v3)+va[i],f4+max(v0,va[i]+v1))); } } int main(){ int n,i,u,v,vv;n=in(); memset(fi,-60,sizeof(fi)); for (i=1;i<n;++i){ u=in();v=in();vv=in(); add(u,v,(LL)vv); }dp(1,0); printf("%I64d\n",max(fi[1][0],max(fi[1][2],fi[1][3]))); }
bzoj4557 侦查守卫
题目大意:每个守卫可以覆盖距离d以内的点,问覆盖树上m个给定点的最小代价。
思路:fi[i][j]表示i的子树都覆盖,至少还能覆盖j的距离的最小代价;gi[i][j]表示i的子树中,至多距离j内的没有覆盖的最小代价。转移的时候用sm[i]表示子树中之多j没被覆盖的和,更新的时候考虑这个点放活着不放,放的话fi[u][d]=sm[d]+wi[u];用某个孩子的fi[i][j]能覆盖其他孩子更新fi[u][j-1],gi[u][j]的更新可以直接用sm[j],对于不用覆盖的fi[u][0]也可以用sm[0]更新。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 500005 #define LL long long #define M 21 #define inf 1000000000000LL using namespace std; int point[N],next[N<<1],en[N<<1],tot=0,d,wi[N],fl[N]={0}; LL fi[N][M],gi[N][M],sm[M]; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u;} void dp(int u,int fa){ int i,j,v,sz=0; for (i=0;i<=d;++i) fi[u][i]=gi[u][i]=inf; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dp(v,u);++sz; }if (!sz){ for (i=0;i<=d;++i){fi[u][i]=wi[u];gi[u][i]=0;} if (!fl[u]) fi[u][0]=0; gi[u][0]=inf;return; }memset(sm,0,sizeof(sm)); for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; for (j=0;j<=d;++j) sm[j]=min(inf*2LL,sm[j]+min(fi[v][0],gi[v][j])); }fi[u][d]=min(fi[u][d],wi[u]+sm[d]); for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; for (j=0;j<d;++j) fi[u][j]=min(fi[u][j],sm[j]-min(fi[v][0],gi[v][j])+fi[v][j+1]); }for (i=1;i<=d;++i) gi[u][i]=min(gi[u][i],sm[i-1]); if (!fl[u]) fi[u][0]=min(fi[u][0],sm[0]); for (i=d-1;i>=0;--i) fi[u][i]=min(fi[u][i],fi[u][i+1]); for (i=1;i<=d;++i) gi[u][i]=min(gi[u][i],gi[u][i-1]); } int main(){ int n,m,i,u,v;n=in();d=in(); for (i=1;i<=n;++i) wi[i]=(LL)in(); m=in(); for (i=1;i<=m;++i) fl[in()]=1; for (i=1;i<n;++i){u=in();v=in();add(u,v);} dp(1,0); printf("%I64d\n",fi[1][0]); }
bzoj4593 聚变反应炉
题目大意:给定一棵树,每个点有一个激发能量di,激发之后可以向联通的点传送能量ci,问激发所有点的最小能量和。(两档数据:1)n<=100000,ci<=1;2)n<=2000,ci<=5)
思路:fi[i]表示i先炸、父亲后炸;gi[i]表示i后炸、父亲先炸。第一档的数据可以直接贪心,按fi[i]-gi[i](fi一定>=gi)排序,更新fi、gi;第二档的数据用背包。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100005 #define M 200005 #define inf 1000000000 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} struct use{ int v,fg; bool operator<(const use&x)const{return fg<x.fg;} }uu[N]; int point[N]={0},next[M],en[M],ci[N]={0},di[N],tot=0,fi[N],gi[N],cc[N]; void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u; } void dp(int u,int fa){ int i,j,v,sz=0;fi[u]=gi[u]=inf; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dp(v,u);sz+=ci[v]; }cc[0]=0; for (i=1;i<=sz;++i) cc[i]=inf; sz=0; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; sz+=ci[v]; for (j=sz;j>=0;--j) cc[j]=min(cc[j]+gi[v],cc[max(0,j-ci[v])]+fi[v]); }for (i=sz;i>=0;--i) fi[u]=min(fi[u],cc[i]+(i>=di[u] ? 0 : di[u]-i)); int up=max(0,di[u]-ci[fa]); for (i=sz;i>=0;--i) gi[u]=min(gi[u],cc[i]+(i>=up ? 0 : up-i)); } void dp2(int u,int fa){ int i,v,sz=0,sm=0;fi[u]=gi[u]=inf; for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; dp2(v,u); }for (i=point[u];i;i=next[i]){ if ((v=en[i])==fa) continue; if (ci[v]) uu[++sz]=(use){v,fi[v]-gi[v]}; sm+=gi[v]; }sort(uu+1,uu+sz+1);uu[0].fg=0; for (i=0;i<=sz;++i){ if (i) uu[i].fg+=uu[i-1].fg; fi[u]=min(fi[u],sm+uu[i].fg+max(0,di[u]-i)); gi[u]=min(gi[u],sm+uu[i].fg+max(0,di[u]-ci[fa]-i)); } } int main(){ int n,i,u,v;n=in(); for (i=1;i<=n;++i) di[i]=in(); for (i=1;i<=n;++i) ci[i]=in(); for (i=1;i<n;++i){u=in();v=in();add(u,v);} if (n<=2000) dp(1,0); else dp2(1,0); printf("%d\n",fi[1]); }
bzoj1495 网络收费(!!!)
题目大意:给出一棵n+1层的满二叉树,叶子节点有种类A/B,叶子节点(i,j)之间有代价:在lca处,如果子树中A多于B,ij均为A代价0,ij一个为B代价为fij,ij均为B代价为2*fij;B多于A的时候类似。每个叶子节点修改种类的代价为ci,问最小的修改代价和代价之和。
思路:代价的计算方式相当于A多的时候B+代价,B多的时候A+代价。fi[i][j][k]表示i这个点的子树中,祖先AB关系为j(二进制,为0表示A<B,为1表示A>=B),子树中有k个B的最小代价。在叶子节点的地方计算出所有的代价,祖先的地方合并不同子树的代价。j和k在一个点的地方是2^(n+1)的,这个可以同时算时间和空间复杂度,可以合并j和k为一维,对于每个点,儿子的个数是平均O(n)的,复杂度是O(2n*2^(2n+2))。
注意:分析时空复杂度的时候注意满二叉树的一些特殊性质,很多时候的复杂度都会优很多。
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define N 1050 #define M 2050 #define up 11 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} int n,ai[M],bi[M],bz=0,ci[M],di[M],fi[M][M],hi[M][up],gi[M][M],inf; int idx(int dep,int j,int k){return (j<<(n-dep+2))|k;} void add(int &x,int y){if (x>y) x=y;} void dp(int u,int dep){ int i,j,k,cc; if (dep==n+1){ for (i=0;i<=1;++i) for (k=0;k<(1<<n);++k){ cc=(i!=ai[u])*ci[u]; for (j=1;j<=n;++j) if (((k>>(j-1))&1)==i) cc+=hi[u][j]; gi[u][idx(dep,k,i)]=cc; }return; }int li,ri,sz,rr,ld,rd,ud,v; dp(u<<1,dep+1);dp(u<<1|1,dep+1); sz=1<<(n-dep+1); for (i=0;i<=1;++i){ if (!i){li=1<<(n-dep)|1;ri=sz;} else{li=0;ri=1<<(n-dep);} for (j=0;j<(1<<(dep-1));++j) for (k=li;k<=ri;++k){ rr=min(k,sz>>1); ud=idx(dep,j,k); for (v=max(0,k-(sz>>1));v<=rr;++v){ ld=idx(dep+1,j<<1|i,v); rd=idx(dep+1,j<<1|i,k-v); if (gi[u<<1][ld]<inf&&gi[u<<1|1][rd]<inf) add(gi[u][ud],gi[u<<1][ld]+gi[u<<1|1][rd]); } } } } int main(){ int i,j,u,ans;n=in(); for (i=1;i<=(1<<n);++i){ bi[i]=i+(1<<n)-1; ai[bi[i]]=in(); }for (i=1;i<=(1<<n);++i) ci[bi[i]]=in(); for (i=1;i<(1<<n);++i) for (j=i+1;j<=(1<<n);++j) fi[bi[i]][bi[j]]=fi[bi[j]][bi[i]]=in(); memset(hi,0,sizeof(hi)); for (i=1;i<=(1<<n);++i){ for (j=(1<<(n+1))-1;j>=(1<<n);--j) di[j]=fi[j][bi[i]]; for (;j;--j) di[j]=di[j<<1]+di[j<<1|1]; for (j=1,u=bi[i];j<=n;++j,u>>=1) hi[bi[i]][j]=di[u^1]; }memset(gi,127,sizeof(gi)); inf=gi[0][0];dp(1,1); for (ans=inf,i=0;i<=(1<<n);++i) add(ans,gi[1][idx(1,0,i)]); printf("%d\n",ans); }
bzoj1304 叶子的染色
题目大意:给出m个点的一棵树,其中1~n是叶子,叶子有0/1的颜色,选一个非叶子的点为根,每个叶子到根路径上离叶子最近的有颜色的点的颜色为叶子的颜色,问最小染色数。
思路:暴力的方法是枚举根,然后树形dp,fi[i][0/1]表示i这个点染0/1满足叶子的所有条件的最小染色数,fi[u][j]=sigma(min(fi[v][j]-1,fi[v][j^1]))+1。然后有一些性质:1)一条边的两边的颜色肯定不同;2)同一条边的两端点为根的答案一样,所以可以制作一遍dp就可以了(!)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 10005 #define M 20005 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} int ci[N],fi[N][2],point[N]={0},next[M],en[M],tot=0,n,m; void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u; } void dp(int u,int ff){ if (u<=n){ fi[u][ci[u]]=1;fi[u][ci[u]^1]=m; return; }int i,v; fi[u][0]=fi[u][1]=1; for (i=point[u];i;i=next[i]){ if ((v=en[i])==ff) continue; dp(v,u); fi[u][0]+=min(fi[v][0]-1,fi[v][1]); fi[u][1]+=min(fi[v][1]-1,fi[v][0]); } } int main(){ int i,u,v; m=in();n=in(); for (i=1;i<=n;++i) ci[i]=in(); for (i=1;i<m;++i){u=in();v=in();add(u,v);} dp(n+1,0); printf("%d\n",min(fi[n+1][0],fi[n+1][1])); }
括号序列表示法(!!!)
bzoj1095 捉迷藏
题目大意:给定一棵树,支持:1)开一个房间的灯;2)查询树上最远的没开灯的两点的距离。
思路:动态树分治的模板题,但写起来非常麻烦,所以学习了括号序列表示的方法。把一棵树按dfs序表上左右括号和点的编号,可以发现,两点间去掉匹配的括号形成的)))((形式可以用数对(a,b)表示,而a+b就是两点的距离,所以可以用线段树维护这个序列。需要维护的量有:1)一个区间左面开始连续的(b-a)和(b+a)最大值(且这部分之后是一个关灯的房间),一个区间右面开始的(a+b)和(a-b)的最大值(且这部分之前是一个关灯的房间);2)一个区间去掉匹配括号之后的a和b。考虑两个区间(a,b)(c,d)的合并,化式子可以发现这四个量可以更新。注意初始化的时候:区间的答案是-inf,括号和亮灯房间的四个最大值都是-inf,关灯房间的四个最大值是0,相应括号的a、b是1/0。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 300005 #define inf 1000000000 using namespace std; int point[N]={0},next[N],en[N],tot=0,tt=0,kd[N],nw[N],pos[N]; struct use{ int ls,ld,rs,rd,lc,rc,ans; void init(int x){ ans=-inf;lc=rc=0; if (kd[x]>0&&nw[kd[x]]) ls=ld=rs=rd=0; else ls=ld=rs=rd=-inf; if (kd[x]==-1) rc=1; if (kd[x]==-2) lc=1; } }tr[N<<2]; char in(){ char ch=getchar(); while(ch<'A'||ch>'Z') ch=getchar(); return ch;} void add(int u,int v){ next[++tot]=point[u];point[u]=tot;en[tot]=v; next[++tot]=point[v];point[v]=tot;en[tot]=u;} void dfs(int u,int f){ int i,v;kd[++tt]=-1; kd[pos[u]=++tt]=u; for (i=point[u];i;i=next[i]){ if ((v=en[i])==f) continue; dfs(v,u); }kd[++tt]=-2;} use updata(use x,use y){ use c; c.ans=max(x.ans,max(y.ans,max(x.rs+y.ld,x.rd+y.ls))); c.rs=max(y.rs,max(x.rs-y.lc+y.rc,x.rd+y.lc+y.rc)); c.rd=max(y.rd,x.rd+y.lc-y.rc); c.ld=max(x.ld,y.ld-x.lc+x.rc); c.ls=max(x.ls,max(y.ls+x.lc-x.rc,y.ld+x.lc+x.rc)); if (x.rc>y.lc){c.lc=x.lc;c.rc=y.rc+x.rc-y.lc;} else {c.lc=x.lc+y.lc-x.rc;c.rc=y.rc;} return c;} void build(int i,int l,int r){ if (l==r){tr[i].init(l);return;} int mid=(l+r)>>1; build(i<<1,l,mid);build(i<<1|1,mid+1,r); tr[i]=updata(tr[i<<1],tr[i<<1|1]);} void tch(int i,int l,int r,int x){ if (l==r){tr[i].init(l);return;} int mid=(l+r)>>1; if (x<=mid) tch(i<<1,l,mid,x); else tch(i<<1|1,mid+1,r,x); tr[i]=updata(tr[i<<1],tr[i<<1|1]);} int main(){ int i,j,n,m,u,v,nt=0; char ch;scanf("%d",&n); for (i=1;i<n;++i){scanf("%d%d",&u,&v);add(u,v);} for (i=1;i<=n;++i) nw[i]=1; dfs(1,0);build(1,1,tt); scanf("%d",&m);nt=n; for (i=1;i<=m;++i){ ch=in(); if (ch=='G') printf("%d\n",(nt<=1 ? nt-1 : tr[1].ans)); else{ scanf("%d",&j); nt+=(!nw[j] ? 1 : -1); nw[j]^=1;tch(1,1,tt,pos[j]); } } }
dfs序和bfs序
bzoj3244 树的计数(!!!)
题目大意:给出dfs序和bfs序,求所有满足两种序的树高的期望。(两种顺序中儿子的先后顺序不变)
思路:为了方便统计,先按bfs的顺序重新标号,记录dfs序ai和dfs序中i的位置pos。对于bfs序分段,分的段数就是树高。
有以下条件:1)1单独一个段;2)一段[l,r]中,pos[l]<pos[l+1]<...<pos[r];3)dep[ai[i+1]]<=dep[ai[i]]+1。
另xi[i],当i和i+1不是一段是xi[i]=1,所以xi的和+1就是树高。
条件可以转化成:1)xi[1]=1;2)如果pos[i]>pos[i+1],xi[i]=1;3)如果ai[i]<ai[i+1],sigma(j=ai[i]~ai[i+1]-1)xi[j]<=1(sigma xi[j]是ai[i]和ai[i+1]的高度差),如果有一个xi确定,其他的是0;如果没有确定的,会发现pos[ai[i]]<pos[ai[i]+1]<...<pos[ai[i+1]],也就是i<i+1,中间只有一个数,所以期望是0.5。这些约束条件可以用差分维护。
注意:xi是1~n-1的数组,表示i和i+1的关系。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LD double #define N 200005 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} int ai[N],bi[N],ci[N],pos[N],fi[N]={0},sm[N]={0},xi[N]={0}; int main(){ int n,i,j;LD ans=1.;n=in(); for (i=1;i<=n;++i) ai[i]=in(); for (i=1;i<=n;++i) ci[bi[i]=in()]=i; for (i=1;i<=n;++i){ai[i]=ci[ai[i]];pos[ai[i]]=i;} xi[1]=1;++fi[1];--fi[2]; for (i=1;i<n;++i) if (pos[i]>pos[i+1]){xi[i]=1;++fi[i];--fi[i+1];} for (i=1;i<=n;++i) sm[i]=sm[i-1]+xi[i]; for (i=1;i<n;++i) if (ai[i]<ai[i+1]){ j=sm[ai[i+1]-1]-sm[ai[i]-1]; if (j){++fi[ai[i]];--fi[ai[i+1]];} } for (j=0,i=1;i<n;++i){ j+=fi[i]; if (j) ans+=(LD)xi[i]; else ans+=0.5; }printf("%.3f\n",ans-0.001); printf("%.3f\n",ans); printf("%.3f\n",ans+0.001); }