[基本操作]線段樹分治和動態dp
不知道為什麽要把這兩個沒什麽關系的算法放到一起寫...可能是都很黑科技?
1.線段樹分治
例題:bzoj4026 二分圖
給你一個圖,資瓷加一條邊,刪一條邊,詢問當前圖是不是二分圖
如果用 LCT 的話我們要維護關於刪除時間的最大生成樹,然後每進來一條邊判斷奇環,就很難寫
線段樹分治可以很好的解決這種有插入有刪除單點詢問的問題
如果沒有加入邊和刪除邊的操作,顯然我們可以用帶權並查集判斷奇環解決
然後我們就要考慮怎麽把帶加入和帶刪除的問題轉換成沒有加入和刪除的問題
我們記一下每條邊存在的時間區間,把這些時間區間放到線段樹上
對於線段樹的每一個區間,我們加入時間區間正好完全包含這個區間的邊,判斷有沒有奇環
有奇環,這一段全都是 No ,沒有奇環,我們遞歸到左右區間分別判斷
如果遞歸到了 $l == r$ 也就是一個時間的單點,還沒有奇環的話,這個單點就是 Yes
中間判斷奇環的部分,一個節點所用到的邊是它和它所有祖先的邊,所以處理完一個區間要把多余的邊刪掉
這個可以用按秩合並的可撤銷並查集,這個並查集還要維護每個點到根節點距離的奇偶性
這樣做的好處是,把帶插入帶刪除的問題,轉換成了沒有插入沒有刪除的 sb 題
復雜度的話,就是一個線段樹區間修改 + 整體詢問的復雜度
所以是 $O(nlogn)$ 的
#include<bits/stdc++.h> #defineView CodeLL long long #define SET vector<edge> using namespace std; const int maxn = 2e5 + 10; inline int read() { int x = 0,f = 1;char ch = getchar(); for(;!isdigit(ch);ch = getchar())if(ch == ‘-‘)f = -f; for(;isdigit(ch);x = 10 * x + ch - ‘0‘,ch = getchar()); return x * f; }int n,m,T; struct edge { int u,v,l,r; bool operator < (const edge &b)const { if(l == b.l) return r < b.r; return l < b.l; } }; SET S; int top = 0; namespace ufsufs { struct node{int fa,val,size;}t[maxn]; struct info{int x,y;node a,b;}st[maxn]; inline void init(){for(int i=1;i<=n;i++) t[i] = (node){i, 0, 1};} inline int find(int x){while(t[x].fa != x) x = t[x].fa;return x;} inline int dis(int x) { int ret = 0; for(;t[x].fa != x;x = t[x].fa)ret ^= t[x].val; return ret; } inline void link(int x,int y) { int val = dis(x) ^ dis(y) ^ 1; x = find(x),y = find(y); st[++top] = (info){x,y,t[x],t[y]}; if(t[x].size > t[y].size)swap(x,y); t[x].fa = y;t[x].val = val;t[y].size += t[x].size; } inline void rec(int bot) { while(top != bot) { info &now = st[top--]; t[now.x] = now.a;t[now.y] = now.b; } } } using namespace ufsufs; inline void SegDiv(int l,int r,SET &S) { int mid = (l + r) >> 1,bot = top; SET ls,rs; for(int i=0;i<(int)S.size();i++) { edge &now = S[i]; int x = now.u,y = now.v; if(now.l == l && now.r == r) { int fx = find(x),fy = find(y); if(fx == fy) { int val = dis(x) ^ dis(y); if(!val) { for(int i=l;i<=r;i++)puts("No"); rec(bot);return; } } else link(x,y); } else if(now.r <= mid)ls.push_back(now); else if(now.l > mid)rs.push_back(now); else ls.push_back( (edge){now.u, now.v, now.l, mid} ), rs.push_back( (edge){now.u, now.v, mid+1, now.r} ); } if(l == r)puts("Yes"); else SegDiv(l,mid,ls),SegDiv(mid + 1,r,rs); rec(bot); } int main() { n = read(),m = read(),T = read(); for(int i=1;i<=m;i++) { int u = read(),v = read(),l = read() + 1,r = read(); if(l > r)continue; S.push_back((edge){u,v,l,r}); } init(); SegDiv(1,T,S); }
其他的一些題
wzj 的兩道題 paint & route
bzoj4137 火星商店問題 可持久化 Trie 樹 + 線段樹分治
loj121 動態圖連通性(離線可過) 可撤銷並查集 + 線段樹分治
2.動態dp
引入 1 :有 n 個矩陣,q 次操作,支持單矩陣內元素修改,區間查詢矩陣乘積
sol:因為矩陣乘法有結合律,可以用線段樹維護這 n 個矩陣,相當於單點修改,區間查詢
引入 2:有一個 dp (原諒我實在想不到啥正經的 dp 能滿足這個)每一步的轉移關於點權都是線性齊次的,且每步轉移一樣,每一次單點修改一個點的點權,修改後詢問 dp 的答案
sol:因為線性齊次而且每步一樣,我們可以用矩陣加速這個 dp,然後變成了上一道題
引入 3 :樹上最大點權獨立集,q 次操作,支持修改一個點的點權,以及詢問答案
sol:這就有點難了
先考慮不帶修改
定義 $f_{(u,0/1)}$ 為 $u$ 點選 / 不選,它的子樹內最大點權獨立集的點權和
轉移就是
$f_{(u,0)} = \sum_{v∈son[u]}max(f_{(v,0)},f_{(v,1)})$
$f_{(u,1)} = \sum_{v∈son[u]}f_{(v,0)} + val_u$
然後我們發現,只要知道一個子樹裏的所有 $f$ 值,它的 $f$ 值也知道了
我們用 dsu on tree(我沒學過 但 SCX 大神學過 好像是這樣的。。。)的思想
我們每到一個點,就把它下面那條重鏈加到隊列裏,然後把和它只隔一條輕鏈的重鏈加到隊列裏,遞歸下去,等所有點都加進去之後按入隊次序的倒序 dp
根據 dsu on tree(感覺過兩天要學一學啊。。。),這樣做和正常 dp 順序是等價的
這樣我們可以把這個 dp 轉化一下
維護一個 light_dp 和 heavy_dp
根據樹鏈剖分的性質,輕兒子必定是重鏈鏈頭,所以輕兒子的 lightdp 和 heavydp 都算完了
$lightdp_{(u,0)} = \sum_{v∈lightson[u]}max(heavydp_{(u,0)},heavydp_{(u,1)})$
$lightdp_{(u,1)} = \sum_{v∈lightson[u]}heavydp_{(u,0)}$
$heavydp_{(u,0)} = lightdp_{(u,0)} + max(heavydp_{(u,1)},heavydp_{(u,0)})$
$heavydp_{(u,1)} = lightdp_{(u,1)} + heavydp_{(u,0)}$
這樣我們就把樹上的 dp 轉換成了鏈上的 dp,然後變成了上一道題
具體地,我們樹鏈剖分,每個點維護一個轉移矩陣,然後用線段樹快速維護一條重鏈的 dp 值
這樣我們修改到一個點,它重鏈鏈頭的 heavydp 就會改變,相當於線段樹的區間修改
然後這個重鏈鏈頭的父親的 lightdp 會因為它而改變,相當於線段樹的單點修改
我們套用上一題的做法就可以了
修改就是相當於修改一個點到根的 dp 值,根據樹鏈剖分是 $O(nlog^2n)$ 的
[基本操作]線段樹分治和動態dp