【換根DP】小奇的倉庫
朋友圈
題目背景
小奇採的礦實在太多了,它準備在喵星系建個礦石倉庫。令它無語的是,喵星系的貨運飛船引擎還停留在上元時代!
題目內容
喵星系有\(n\)個星球,星球以及星球間的航線形成一棵樹。
從星球\(a\)到星球\(b\)要花費\([\text{dis}(a,b)\ \text{xor}\ M]\)秒。(\(\text{dis}\)(\(a,b\))\(表示ab間的航線長度,\)\text{xor}$為位運算中的異或)
為了給倉庫選址,小奇想知道,星球\(i\)(\(1\leq i\leq n\))到其它所有星球花費的時間之和。
資料範圍
\(6\leq n\leq 100000,0\leq M\leq 15\)
思路
出題人:
演算法1:
不會寫函式的小夥伴們,我們只需要寫個floyd,就有10分啦!
演算法2:
在演算法1的基礎上,我們對每條邊處理一下xor,就有20分啦!
演算法3:
簡單的樹形DP,或者你會nlogn的dij,處理完每個點到其它點的最短路後再加上xor,那麼這樣就有30分啦!
演算法4:
第4、5個點無需xor,那麼我們樹形DP掃一個節點與其它所有節點的路徑長度之和,可以合併資訊,最終均攤O(1),50分到手。
演算法5:
第6個點xor 1,那麼我們樹形DP到一個點時記錄有多少個0,多少個1,然後每當一條路徑到2,那部分就再記錄一個值,60分到手。
演算法6:
如果你第6個點都過了,卻沒有滿分,笨死啦!
一樣的嘛,就是原來的“0”、“1”、大於等於2變成了0~16麼~~
滿了。
我:?
考場上直接打的\(O(n^2)\)列舉區間再加上求\(\text{LCA}\)的複雜度的暴力,結果一時腦癱建邊的時候就異或了\(M\)結果慘掛\(10pts\),然後考後改成最後再異或就\(30pts\)了...(~差點有比郭神高的機會呢qwq)
然後正解是換根\(dp\),又是假期埋下的一個坑嗎
先考慮沒有異或的情況。設已經搜到了邊\(<u,v,w>\),且\(u\)是\(v\)的父親,那麼如何更新\(ans[v]\)呢?
\[ans[v]=ans[u]+(n-size[v])\times w-size[v]\times w \]
當然你會合並同類項一下,不過先不合並理解一下,對於\(u\)
大概這個樣子:
然而本題要求異或,由我慘掛10分的經歷可以知道異或並不滿足分配律,所以並不能邊加邊異或。然而可以看出\(M\leq 15\),轉換為二進位制為\(1111\),所以最後異或\(M\)的時候僅會對後四位有影響,所以只需要記錄後四位的狀態即可。
設\(f[i][j]\)表示到了\(i\)點,當前後四位的狀態為\(j\),能伸展出的路徑條數。
對於初始:\(f[u][0]=1\),表示自己到自己為一條路徑。為了方便,你可以先加上自己然後最後減去。
然後就是\(f[u][(j+w)\ \%\ 16]+=f[v][j]\),從子樹轉移過來。
然而出了子樹以外還有別的點,如何轉移呢?
\[f[v][(j+w)\ \%\ 16]+=(f[u][j]-f[v][(j-w)\ \%\ 16]) \]
很容易理解,對於\(v\)的父親\(u\),先刨去子樹\(v\)的貢獻,然後剩下的就是其他點到\(u\)的貢獻,你再通過\(<u,v,w>\)邊轉移到\(v\)上,再加上原來就有的\(v\)的子樹的,就是整棵樹到\(v\)的貢獻。
最後你再異或個\(M\)即可。
程式碼
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,M;
long long f[maxn][20],ans[maxn],a[20];
struct Edge{
int from,to,w,nxt;
}e[maxn<<1];
inline int read(){
int x=0,fopt=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')fopt=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-48;
ch=getchar();
}
return x*fopt;
}
int head[maxn],cnt;
inline void add(int u,int v,int w){
e[++cnt].from=u;
e[cnt].to=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
}
void dfs1(int u,int fa){
f[u][0]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa)continue;
dfs1(v,u);
ans[u]+=ans[v];
for(int j=0;j<=15;j++){
int w=e[i].w;
f[u][(j+w)%16]+=f[v][j];
ans[u]+=f[v][j]*w;
}
}
}
void dfs2(int u,int fa){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,w=e[i].w;
if(v==fa)continue;
memset(a,0,sizeof(a));//臨時先開個陣列存一下,因為下面還要加siz,最好不要直接更新
int siz=0;
for(int j=0;j<=15;j++){
a[(j+w)%16]+=f[u][j]-f[v][((j-w)%16+16)%16];//防止下標變負
siz+=f[v][j];
}
ans[v]=ans[u]+(n-2*siz)*w;
for(int j=0;j<=15;j++)
f[v][j]+=a[j];
dfs2(v,u);
}
}
int main(){
freopen("B.in","r",stdin);
freopen("B.out","w",stdout);
n=read();M=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
add(u,v,w);
add(v,u,w);
}
dfs1(1,0);
dfs2(1,0);
for(int i=1;i<=n;i++){
f[i][0]--;//刨去到自己的路徑
for(int j=0;j<=15;j++)
ans[i]+=((j^M)-j)*f[i][j];//加上異或後相差的值,另外還是老問題異或的優先順序
printf("%lld\n",ans[i]);
}
return 0;
}