1. 程式人生 > 實用技巧 >線段樹合併

線段樹合併

線段樹合併

前置芝士

動態開點線段樹權值線段樹

乍一看,線段樹合併和上面那兩個奇怪的東西有什麼關係。

其實,線段樹合併的全稱為動態開點權值線段樹合併( 霧

如果對上面那兩個奇怪的東西不理解可點開連結進行搜尋(大霧

優點

​ 動態開點線段樹有著一些優點,比如說當你讓某個節點繼承另一個節點的左兒子或者右兒子的時候,你可以不用新建一棵線段樹,而是直接將該節點的左右兒子賦成那個節點的左右兒子就行了,總之就是空間上有一定的優越性。

​ 權值線段樹能代替平衡樹做一些求 kk 大、排名、找前驅後繼的操作。(顯然是我不會平衡樹,如果你會平衡樹當我沒說)

概念

線段樹合併,顧名思義,就是建立一棵新的線段樹儲存原有的兩顆線段樹的資訊。

合併方式主要如下:

\[\begin{aligned}\boxed {對於兩棵線段樹都有的節點,新的線段樹的該節點值為兩者和。\\ 對於某一棵線段樹有的節點,新的線段樹儲存該節點的值。\\ 然後對左右子樹遞迴處理。 }\end{aligned} \]

如果不能理解,可以往下翻看程式碼

所以問題來了,複雜度是多少?

複雜度

\[\begin{aligned}\boxed{先來思考一下在動態開點線段樹中插入一個點會加入多少個新的節點\\ 線段樹從頂端到任意一個葉子結點之間有 logn 層,每層最多新增一個節點\\ 所以插入一個新的點複雜度是 logn 的\\ 兩棵線段樹合併的複雜度顯然取決於兩棵線段樹重合的葉子節點個數,假設有 m 個\\重合的點,這兩棵線段樹合併的\\複雜度就是 mlogn 了,\textbf{所以說,如果要合併兩棵滿}\\\textbf{滿的線段樹},這個複雜度絕對是遠大於 logn 級別的。\\ 也就是說,千萬不要以為線段樹合併對於任何情況都是 logn 的!\\ 那麼為什麼資料範圍 10^5 的題目線段樹合併還穩得一批?\\ 這是因為logn 的複雜度僅適用於插入點少的情況。\\ 如果 n 與加入的總點數規模基本相同,我們就可以把它理解成每次操作 O(logn)\\ 來證明一下:\\ 假設我們會加入 k 個點,由上面的結論,我們可以推出最多要新增 klogk 個點。\\ 而正如我們所知,每次合併兩棵線段樹同位置的點,就會少掉一個點,複雜度為 \\O(1) ,總共 klogk 個點,全部合併的複雜度就是 O(klogk)\\ 可見,上面那個證明是隻與插入點個數 kk 有關,也就是插入次數在 10^5左右、值域 \\10^5左右的題目,線段樹合併還是比較穩的。\\ 至於更快的方法?\\ 網上有說可以用可並堆的思路合併,我太菜了,並沒有試過,所以就點到為止了~\\ 對了,由上可知,因為插入 klogk 個節點,所以線段樹合併的空間複雜度也是 \\O(klogk) 的}\end{aligned} \]

(轉自洛穀日報)

程式碼

合併(好像也就這一個操作,別的和動態開點,權值線段樹一樣)

//tot在這裡面就是記錄編號的 ls和rs為lson和rson
int merge(int a,int b,int l,int r){
    if(!a) return b;
    if(!b) return a;
    //可寫成 if(!a||!b) return a|b;
    if(l==r) 
    {
        sum[++tot]=sum[a]+sum[b];
        return tot;
    }
    int mid=(l+r)>>1;
    //這裡省略若干操作,因題而異
    ls[++tot]=merge(ls[a],ls[b],l,mid);
    rs[tot]=merge(rs[a],rs[b],mid+1,r);
    sum[tot]=sum[ls[tot]]+sum[rs[tot]];
    return tot;
}

例題1

CF600E
思路

線段樹合併。權值線段樹覆蓋顏色1−>100000,用sum1−>100000,用sum表示顏色最多出現的次數,ans表示答案。分3種情況push_up即可。

  1. 左右子樹sum相等
  2. 左邊>右邊
  3. 左邊<右邊

  dfs的時merge一下即可。

程式碼
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector> 
#define mid (l+r>>1)
#define lson tr[i].l
#define rson tr[i].r
#define int long long

using namespace std;
const int maxn=100010;
int col[maxn];
int n,cnt;
int rt[maxn];
vector<int>g[maxn];
int anss[maxn];

struct node{			//這裡使用的儲存方式不是ls[],rs[]而是結構體 
	int l,r,sum,ans;//sum為最多出現顏色次數 ans為最多出現編號 
}tr[maxn*40];

inline void push_up(int i)
{
    if(tr[lson].sum==tr[rson].sum)
    {
        tr[i].sum=tr[lson].sum;
        tr[i].ans=tr[lson].ans+tr[rson].ans;
    }
    else if(tr[lson].sum<tr[rson].sum)
    {
        tr[i].sum=tr[rson].sum;
        tr[i].ans=tr[rson].ans;
    }
    else
    {
        tr[i].sum=tr[lson].sum;
        tr[i].ans=tr[lson].ans;
    }
}

inline void update(int &i,int l,int r,int pos)
{
    if(!i)i=++cnt;
    if(l==r)
    {
        tr[i].sum++;tr[i].ans=l;
        return;
    }
    if(pos<=mid)update(lson,l,mid,pos);
    else update(rson,mid+1,r,pos);
    push_up(i);
}

inline int merge(int a,int b,int l,int r)
{
    if(!a||!b)return a+b;
    if(l==r)
    {
        tr[a].sum+=tr[b].sum;tr[a].ans=l;
        return a;
    }
    tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
    tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
    push_up(a);
    return a;
}

inline void dfs(int now,int fa)
{
    for(int i=0;i<g[now].size();i++)
    {
        if(g[now][i]==fa)continue;
        dfs(g[now][i],now);
        merge(rt[now],rt[g[now][i]],1,100000);
    }
    update(rt[now],1,100000,col[now]);
    anss[now]=tr[rt[now]].ans;
}

signed main()
{
    ios::sync_with_stdio(false);		//cf上用%lld輸入好像不大行,所以改成cin的快讀了
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>col[i];
        rt[i]=i;
		cnt++;
    }
    for(int i=1;i<n;i++)
    {
        int from,to;
		cin>>from>>to;
        g[from].push_back(to);
		g[to].push_back(from);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++)
    {
        cout<<anss[i]<<" ";
    }
    return 0;
}

(這道題除了線段樹合併,還可以用樹上啟發式合併/dsu on tree 來解決,有興趣的讀者可自行搜尋)

例題2

P3521 [POI2011]ROT-Tree Rotations
思路

這道題主要就是權值線段樹合併的一個過程。我們對每個葉子結點開一個權值線段樹,然後逐步合併。

考慮到一件事情:如果在原樹有一個根節點 \(x\),和其左兒子 \(ls\) ,右兒子 \(rs\) 。我們要合併的是 \(ls\) 的權值線段樹和 \(rs\) 的權值線段樹,得到 \(x\) 的所有葉節點的權值線段樹。

發現交換 \(ls\)\(rs\) 並不會對原樹更上層之間的逆序對產生影響,於是我們只需要每次合併都讓逆序對最少。

於是我們的問題轉化為了給定兩個權值線段樹,問把它們哪個放在左邊可以使逆序對個數最小,為多少。

考慮我們合併到一個節點,其權值範圍為 \([l,r]\) ,中點為 \(mid\) 。這個時候我們有兩棵樹,我們要分別計算出某棵樹在左邊的時候和某棵樹在右邊的時候的逆序對個數。事實上我們只需要處理權值跨過中點 \(mid\) 的逆序對,那麼所有的逆序對都會在遞迴過程中被處理僅一次(類似一個分治的過程)。而我們這個時候可以輕易的算出兩種情況的逆序對個數,不交換的話是左邊那棵樹的右半邊乘上右邊那棵樹的的左半邊的大小;交換的話則是左邊那棵樹的左半邊乘上左邊那棵樹的的右半邊的大小。

然後每次合併由於都可以交換左右子樹,我們就把這次合併中交換和不交換的情況計算一下,取最小值累積就可以了。

空間複雜度:\(O(n \log n)\),時間複雜度 \(O(n \log n)\)

#include<cstdio>
#define rep(i, a, b) for (register int i=(a); i<=(b); ++i)
#define per(i, a, b) for (register int i=(a); i>=(b); --i)

using namespace std;
const int N=6000005;
long long min(long long a, long long b){return a<b?a:b;}
int ls[N], rs[N], val[N], n, tot;
long long ans, ans1, ans2;

inline int read()
{
    int x=0,f=1;char ch=getchar();
    for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
    for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
    return x*f;
}

int New(int l, int r, int x)
{
    val[++tot]=1;
    if (l==r) return tot;
    int mid=l+r>>1, node=tot;
    if (x<=mid) ls[node]=New(l, mid, x);
        else rs[node]=New(mid+1, r, x);
    return node;
}

int merge(int l, int r, int u, int v)
{
    if (!u || !v) return u+v;
    if (l==r) {val[u]=val[u]+val[v]; return u;}
    int mid=(l+r)>>1, node=u;
    ans1+=1ll*val[rs[u]]*val[ls[v]]; 
    ans2+=1ll*val[ls[u]]*val[rs[v]];
    ls[node]=merge(l, mid, ls[u], ls[v]);
    rs[node]=merge(mid+1, r, rs[u], rs[v]);
    val[node]=val[ls[node]]+val[rs[node]];
    return node;
}

int dfs()
{
    int v=read();
    if (v) return New(1, n, v);
    int node=merge(1, n, dfs(), dfs());
    ans+=min(ans1, ans2); ans1=ans2=0;
    return node;
}

int main()
{
    n=read(); dfs(); printf("%lld\n", ans);
    return 0;
}