HDU 5977 Garden of Eden (樹形dp+快速沃爾什變換FWT)
CGZ大佬提醒我,我要是再不更博客可就連一月一更的頻率也沒有了。。。
emmm,正好做了一道有點意思的題,就拿出來充數吧=。=
題意
一棵樹,有 $ n (n\leq50000) $ 個節點,每個點都有一個顏色,共有 $ k(k\leq10) $ 種顏色,問有多少條路徑可以遍歷到所有 $ k $ 種顏色?(一條路徑交換起點終點就算兩條哦)
做法
事實證明,連我都能不看題解想出來的題果然都是水題qwq
我是從CJ的xzyxzy大佬上的博客上看到這道題的,所以就理所當然用FWT做了...然後才發現網上的題解都是點分治...Menhera大佬提供了一個更優的做法,不過我是真的看不懂...放在最後講一下(在代碼後面)。
這道題一眼就是樹形dp,而且k特別小,貌似可以狀壓?
用二進制數 $ S $ 表示一條路徑上的顏色種類,用 $ dp[i][S] $ 表示當前節點 $ i $ 到它下面的葉子節點中,顏色狀態為 $ S $ 的路徑的數量。很顯然 $ S=2^k-1 $ 的路徑就是我們要找的路徑,我們的目標就是求出這樣的路徑的數量ヾ(???ゞ)!
求出 $ dp[i][S] $ 是很容易的,只需要遍歷一遍就行了。然而,有的路徑的兩端會在 $ i $ 的兩個子樹中而橫跨 $ i $ 這個結點,這樣的路徑怎麽統計呢?總不能一個個枚舉吧。。這就該FWT上場了!把要統計的兩個子樹的 $ dp[x][S] $ 做or卷積,然後把 $ S=111...1 $ 的路徑條數累加進答案就可以啦!這樣做時間復雜度是 $ O(n2^kk) $ ,而這道題的時限是5s,所以還是可以輕松跑過的。
emmm...(在打了一會代碼之後)
等等,哪裏有點問題...樹上有50000個點,每個點需要大小為1024的數組來存儲狀態,我需要開整整195MB的數組?!
經過一番思考...我終於發現了這樣的方法:先一遍dfs求出每個節點的重兒子,dp時優先遞歸重兒子,然後遞歸別的兒子,一遍FWT求出答案後再將兩個兒子的狀態合並,回收輕兒子的空間,在接著遞歸別的兒子。這樣,可以證明在某一時刻最多同時存在 $ log_2n $ 個兒子的狀態(與樹剖的證明相似),所以空間就只需要開一點點啦~
代碼:(有很詳細的註釋哦qwq不會的可以看一下代碼)
#include <cstdio> #include <cstring> #include <algorithm> #include <stack> #define R register using namespace std; typedef long long LL; const int MAXN=50100; const int MAXM=1100; int he[MAXN],col[MAXN]; int siz[MAXN],son[MAXN]; int dp[100][MAXM]; int n,k,cnt,len; LL ans;//註意ans是有可能爆longlong的 template<class T>int read(T &x)//這是zyf看了會沈默的可以判EOF的快讀 { x=0;int ff=0;char ch=getchar(); while((ch<‘0‘||ch>‘9‘)&&ch!=EOF){ff|=(ch==‘-‘);ch=getchar();} while(ch>=‘0‘&&ch<=‘9‘){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x=ff?-x:x; if(ch==EOF)return EOF; return 1; } class FWT { private: LL tem1[MAXM],tem2[MAXM]; void fwt_or(LL *a,int f)//普通的FWT,但是zyf說or和and卷積都應該叫做快速莫比烏斯變換(FMT)? { for(R int i=1;i<len;i<<=1) for(R int j=0;j<len;j+=(i<<1)) for(R int k=0;k<i;++k) if(f==1)a[j+k+i]+=a[j+k]; else a[j+k+i]-=a[j+k]; } public: int fwt(int *a,int *b) { for(R int i=0;i<len;++i)tem1[i]=a[i]; for(R int i=0;i<len;++i)tem2[i]=b[i]; fwt_or(tem1,1); fwt_or(tem2,1); for(R int i=0;i<len;++i)tem1[i]*=tem2[i]; fwt_or(tem1,-1); return (int)tem1[len-1]; } }fwt; struct edge { int to,next; }ed[MAXN<<1]; void added(int x,int y)//加邊,常規操作 { ed[++cnt].to=y; ed[cnt].next=he[x]; he[x]=cnt; } void dfs_pre(int x,int fa)//求重兒子 { siz[x]=1,son[x]=0; for(int i=he[x];i;i=ed[i].next) { int to=ed[i].to; if(to==fa)continue; dfs_pre(to,x); siz[x]+=siz[to]; if(siz[to]>siz[son[x]])son[x]=to; } } stack<int>stk;//用於回收空間 int dfs(int x,int fa) { int num,bt=1<<col[x]; if(son[x])num=dfs(son[x],x);//繼承重兒子的空間 else//是葉節點 { if(!stk.empty())//使用回收後的空間 num=stk.top(),stk.pop(); else num=++cnt;//使用新空間 dp[num][bt]=1; return num;//上傳空間給父親 } for(R int i=1;i<len;++i)//根據重兒子的狀態推出自己的狀態 if(dp[num][i]) if(!(i&bt)) dp[num][i|bt]+=dp[num][i],dp[num][i]=0; ans+=dp[num][len-1]; dp[num][bt]+=1; for(int j=he[x];j;j=ed[j].next)//枚舉輕兒子們 { int to=ed[j].to; if(to==fa||to==son[x])continue; int tnum=dfs(to,x);//得到輕兒子的狀態 ans+=fwt.fwt(dp[num],dp[tnum]);//FWT並累加答案 for(R int i=1;i<len;++i)//將這個輕兒子的狀態合並至自己的狀態 if(dp[tnum][i]) if(!(i&bt)) dp[num][i|bt]+=dp[tnum][i],dp[tnum][i]=0; else dp[num][i]+=dp[tnum][i],dp[tnum][i]=0; stk.push(tnum);//空間回收 } return num;//上傳空間給父親 } int main() { while(read(n)!=EOF) { memset(he,0,sizeof(he)); memset(dp,0,sizeof(dp)); while(!stk.empty())stk.pop();//各種清零不要忘 read(k); len=1<<k; for(R int i=1;i<=n;++i) read(col[i]),--col[i]; int t1,t2; cnt=0,ans=0; for(R int i=1;i<n;++i)//加邊 { read(t1),read(t2); added(t1,t2); added(t2,t1); } if(k==1)//特判就好了,可以省很多事 { ans=1ll*n*(n-1); ans+=n; printf("%lld\n",ans); continue; } dfs_pre(1,0); cnt=0;//這裏cnt被用於標記當前使用的空間 dfs(1,0); ans<<=1;//別忘了交換起點終點後的路徑算的不同路徑 printf("%lld\n",ans); } return 0; }
什麽?優化?
我一個在明德的好朋友Menhera醬發現可以把復雜度中的 $ k $ 去掉,變成 $ O(n2^k) $ ,具體貌似是利用“基”的形式進行 $ O(2^k) $ 的FWT,具體可以看她的這篇博客:https://www.cnblogs.com/Menhera/p/9514412.html (那不足50行的代碼真是震撼我心)
Menhera:“我的這個做法是什麽點分治的優化版,而你的是弱化版~”(事實是我1700MS她1200MS。。。明明去了一個log然而感覺就像卡了常一樣)
只感覺智商被碾壓qwq。。。Menhera太強了orz
HDU 5977 Garden of Eden (樹形dp+快速沃爾什變換FWT)