題解 P1600 【天天愛跑步】
話說,這道題都 \(55\) 篇題解了,怎麼還能交啊……
不過沒事,反正我也沒想交……
前言
可以發現,這道題跟線段樹合併模板是有著異曲同工之妙的,都是在一條鏈上處理關於權值計數的問題,只不過這道題最後不需要查詢出現次數最多的權值,只需要查詢對應權值的出現次數,所以最後可以不用寫一顆權值線段樹而去用桶來代替。
我們現在這裡膜一下x義x ,他線上段樹合併的版題裡寫了一個非常小清新的離線樹剖做法,也就是我們在這裡即將介紹的方法,實際上懂的人過去看一下他寫的程式碼估計一下子就明白了,但是作者將會在這裡比較清晰的說明一些細節問題(其實巨佬x義x已經講的很明白了),同時也是為了加深演算法的印象。
題解
樹剖的本質,實際上就是將樹上的一條路徑轉化為 \(log~n\)
同理,對於這一類題目,是區間對於點的貢獻,我們也可以考慮這麼做,利用樹剖的優良的拆路徑方法,將路徑轉化為區間的問題。而這一題我們可以近似到“樹上差分”,我們通過樹剖就可以把他轉換為序列的“差分”,就容易處理很多了。
例如,如果我們需要將 \(u\) 到 \(v\) 的路徑上都使 \(w\) 的計數 \(+1\) 。我們可以考慮將路徑差分成一些區間,然後對於每一個位置的修改都可以利用一個連結串列或者 \(vector\)
void work(int u,int v,int w) { while(s[u].top!=s[v].top) { if(s[s[u].top].dep<s[s[v].top].dep) swap(u,v); d[s[s[u].top].mp].push_back(Change{w,1});//使用類似差分的思路,即左端點這個數的計數+1 d[s[u].mp+1].push_back(Change{w,-1});//右端點的下一個位置關於這個數的計數-1 u=s[s[u].top].fa;//常規樹剖跳鏈 } if(s[u].dep>s[v].dep) swap(u,v); d[s[u].mp].push_back(Change{w,1}); d[s[v].mp+1].push_back(Change{w,-1}); }
然後我們就將若干條路徑全部轉化到了 \(dfs\) 序上,然後我們列舉一遍 \(dfs\) 序即可。
//其實可以不用寫線段樹,用桶來維護資訊,只不過不想寫太多程式碼了,就直接從線段樹合併那裡copy過來了
//如果改成桶,時間上是可以節約一個log的
for(int i=1;i<=n;++i)
{
for(int j=0;j<(int)d[0][i].size();++j) t[0].add(1,1,(MAXN-5)<<1,d[0][i][j].type,d[0][i][j].data);
for(int j=0;j<(int)d[1][i].size();++j) t[1].add(1,1,(MAXN-5)<<1,d[1][i][j].type,d[1][i][j].data);
ans[dfn[i]]+=t[0].query(1,1,(MAXN-5)<<1,w[dfn[i]]+s[dfn[i]].dep+MAXN);
ans[dfn[i]]+=t[1].query(1,1,(MAXN-5)<<1,w[dfn[i]]-s[dfn[i]].dep+MAXN);
}
然後你就發現我們已經將最關鍵的部分講完了。可以發現,這種樹剖的寫法不僅程式碼實現簡單而且思路清晰,作用廣泛。
以上。(最後還有內容哦~)
完整程式碼如下:
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5,M=3e5+5,MAXN=3e5+5;
int n,m;
struct Edge{int nxt,to;}e[N<<1];int fir[N];
void add(int u,int v,int i){e[i]=Edge{fir[u],v},fir[u]=i;}
struct Node{int fa,size,dep,son,top,mp;}s[N];
void dfs1(int u)
{
s[u].size=1,s[u].top=u;
for(int i=fir[u];i;i=e[i].nxt)
{
if(e[i].to==s[u].fa) continue;
s[e[i].to].fa=u;
s[e[i].to].dep=s[u].dep+1;
dfs1(e[i].to);
s[u].size+=s[e[i].to].size;
if(s[e[i].to].size>s[s[u].son].size) s[u].son=e[i].to;
}
}
int dfn[N],cnt=0;
void dfs2(int u)
{
dfn[++cnt]=u,s[u].mp=cnt;
if(s[u].son) s[s[u].son].top=s[u].top,dfs2(s[u].son);
for(int i=fir[u];i;i=e[i].nxt)
{
if(e[i].to==s[u].fa||e[i].to==s[u].son) continue;
dfs2(e[i].to);
}
}
struct Change{int type,data;};
vector<Change> d[2][N];
void work(int u,int v,int w,int tag)
{
while(s[u].top!=s[v].top)
{
if(s[s[u].top].dep<s[s[v].top].dep) swap(u,v);
d[tag][s[s[u].top].mp].push_back(Change{w,1});
d[tag][s[u].mp+1].push_back(Change{w,-1});
u=s[s[u].top].fa;
}
if(s[u].dep>s[v].dep) swap(u,v);
d[tag][s[u].mp].push_back(Change{w,1});
d[tag][s[v].mp+1].push_back(Change{w,-1});
}
int lca(int u,int v)
{
while(s[u].top!=s[v].top)
{
if(s[s[u].top].dep<s[s[v].top].dep) swap(u,v);
u=s[s[u].top].fa;
}
if(s[u].dep>s[v].dep) swap(u,v);
return u;
}
struct Seg_Tree
{
struct Node
{
int data,type;
bool operator > (const Node x) const {return data>x.data;}
bool operator < (const Node x) const {return data<x.data;}
}tr[MAXN<<3];
void up(int u){tr[u]=max(tr[u<<1],tr[u<<1|1]);}
void build(int u,int l,int r)
{
if(l==r){tr[u].data=0,tr[u].type=l;return;}
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
up(u);
}
void add(int u,int l,int r,int x,int z)
{
if(x<=l&&r<=x){tr[u].data+=z;return;}
int mid=(l+r)>>1;
if(x<=mid) add(u<<1,l,mid,x,z);
if(x>mid) add(u<<1|1,mid+1,r,x,z);
up(u);
}
int query(int u,int l,int r,int x)
{
if(x<l||x>r) return 0;
if(x<=l&&r<=x) return tr[u].data;
int mid=(l+r)>>1;
if(x<=mid) return query(u<<1,l,mid,x);
if(x>mid) return query(u<<1|1,mid+1,r,x);
return 0;
}
}t[2];
int w[N],ans[N];
int main()
{
cin>>n>>m;
for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),add(u,v,i<<1),add(v,u,i<<1|1);
s[1].dep=1;
dfs1(1),dfs2(1);
for(int i=1;i<=n;++i) scanf("%d",&w[i]);
for(int i=1,u,v;i<=m;++i)
{
scanf("%d%d",&u,&v);
int tmp=lca(u,v),dis=s[u].dep+s[v].dep-s[tmp].dep*2;
work(u,tmp,s[u].dep+MAXN,0);
work(v,tmp,dis-s[v].dep+MAXN,1);
if(s[u].dep==s[tmp].dep+w[tmp]) ans[tmp]--;
}
t[0].build(1,1,(MAXN-5)<<1);
t[1].build(1,1,(MAXN-5)<<1);
for(int i=1;i<=n;++i)
{
for(int j=0;j<(int)d[0][i].size();++j) t[0].add(1,1,(MAXN-5)<<1,d[0][i][j].type,d[0][i][j].data);
for(int j=0;j<(int)d[1][i].size();++j) t[1].add(1,1,(MAXN-5)<<1,d[1][i][j].type,d[1][i][j].data);
ans[dfn[i]]+=t[0].query(1,1,(MAXN-5)<<1,w[dfn[i]]+s[dfn[i]].dep+MAXN);
ans[dfn[i]]+=t[1].query(1,1,(MAXN-5)<<1,w[dfn[i]]-s[dfn[i]].dep+MAXN);
// printf("%d %d %d\n",dfn[i],t[0].query(1,0,(MAXN-5)<<1,s[dfn[i]].dep+w[dfn[i]]+MAXN),t[1].query(1,0,(MAXN-5)<<1,s[dfn[i]].dep-w[dfn[i]]+MAXN));
}
for(int i=1;i<=n;++i) printf("%d ",ans[i]);
printf("\n");
return 0;
}
後記
其實寫這麼一篇題解更多的是希望能夠關注到樹剖的這一種優秀的思路,因為將樹上路徑問題轉化為區間問題後實際上很多東西都能迎刃而解。所以希望這一篇題解能使閱讀他的人更多的開啟做題的思路。
實際上關於樹上問題轉化為路徑問題的還有利用尤拉序來解決的,但是兩者的側重點不一樣。數剖更側重於區間的修改操作。而尤拉序更側重於區間的計數或統計操作,更常見於與莫隊演算法配套(作者前面有文章進行了簡略的介紹)。總之,能過題的方法都是好方法。