線段樹差分及其應用


簡述概念和應用

  所謂的差分,其實就是后一項與前一項的差,對於第一項而言,\(a[0] = 0\) 。設數組 \(a[~]=\{1,9,3,5,2\}\) ,那么差分數組\(t[~]=\{1,8,-6,2,-3\}\) ,即 \(t[i]=a[i]-a[i-1]\) ,那么,

\[a[i]=t[1]+...+t[i] \]

  差分在線段樹和樹狀數組上應用很廣泛。關於樹狀數組的差分可以用來解決“區間修改,單點查詢”的問題,在我上一篇博客講樹狀數組入門時有分析,題目是P3368 【模板】樹狀數組 2。而對於線段樹,我們可以考慮對差分數組進行區間維護,比如維護差分數組的區間最大值,即原數組對應區間相鄰元素的最大差值

例題一 求差分最值

NC14402 求最大值
  這道題首先要將問題轉化,我們不能直接維護這個最大值。如果把問題放到一個二維坐標系,數組下標是橫坐標,那么原數組對應的值是縱坐標,這題就是求兩點之間最大斜率。畫圖就可以知道這個最大斜率只可能出現在相鄰的兩個點之間。問題就變簡單了,由上面的結論,我們只要維護出差分數組的區間最大值即可。注意,這個時候線段樹的葉子結點變成了原數組的相鄰兩點的差,不是原數組的某個值。
  我們再來看題目要求的修改方式,是單點修改。那么一個點改變就會改變差分數組中的兩個點(如果不是第一個點或者最后一個點的話,這兩點需特判)。那么,我們的思路就是建立一棵差分數組作為最底層的線段樹(這題並不用懶標記),每次修改就要修改最底層的兩個點。來看一下建樹操作,注意樹根節點的管轄的范圍是 \([2,n]\),因為 題目要求 \((a[j]-a[i])/(j-i),1<=i<j<=n\),即 \(j > 1\) 。按照這個思路就可以自己打代碼了,如果覺得細節上還有所欠缺可以繼續往下看。

#define ls now<<1
#define rs now<<1|1
#define mid (l+r)/2
int  t[maxn<<2],n,m,num[maxn];

void build(int now,int l,int r){
    if(l == r) {t[now] = num[l] - num[l - 1];return;}
    build(ls,l,mid);
    build(rs,mid+1,r);
    t[now] = max(t[ls],t[rs]);
}

  注意這道題是修改一下就查詢一下,查詢的最大值其實就已經保留在線段樹根節點了。按照我們之前說的,修改要修改線段樹中兩個葉子結點,並且特判是不是第一個點和后一個點。主函數中部分代碼而下:

      scanf ("%d%d", &pos, &value);
      num[pos] = value;             //原數組修改
      if(pos > 1) update(1,2,n,pos,num[pos] - num[pos - 1]);   //如果不是第一個點
      if(pos < n) update(1,2,n,pos+1,num[pos + 1] - num[pos]);  //如果不是最后一個點
      double tem = t[1];
      printf("%.2lf\n",tem);

  最后是修改操作,和普通線段樹修改差不多,注意這里不需要懶標記,也就沒有 \(\mathrm{pushdown}\) 操作了。

void update(int now,int l,int r,int pos,int value){
    if(l == r) {
        t[now] = value;
        return;
    }
    if(pos <= mid) update(ls,l,mid,pos,value);
    else  update(rs,mid+1,r,pos,value);
    t[now] = max(t[ls],t[rs]);
}

\(Code\)

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define ls now<<1
#define rs now<<1|1
#define mid (l+r)/2
const int maxn = 2e5+11;
int  t[maxn<<2],n,m,num[maxn];

void build(int now,int l,int r){
    if(l == r) {t[now] = num[l] - num[l - 1];return;}
    build(ls,l,mid);
    build(rs,mid+1,r);
    t[now] = max(t[ls],t[rs]);
}

void update(int now,int l,int r,int pos,int value){
    if(l == r) {
        t[now] = value;
        return;
    }
    if(pos <= mid) update(ls,l,mid,pos,value);
    else  update(rs,mid+1,r,pos,value);
    t[now] = max(t[ls],t[rs]);
}

int main(){
    while(~scanf ("%d", &n)){
        For(i,1,n) scanf ("%d", num+i);
        build(1,2,n);  //注意建樹范圍,從2到n
        scanf ("%d", &m);int pos,value;
        For(i,1,m){
            scanf ("%d%d", &pos, &value);
            num[pos] = value;             //原數組修改
            if(pos > 1) update(1,2,n,pos,num[pos] - num[pos - 1]);   //如果不是第一個點
            if(pos < n) update(1,2,n,pos+1,num[pos + 1] - num[pos]);  //如果不是最后一個點
            double tem = t[1];
            printf("%.2lf\n",tem);
        }
    }
    return 0;
}

  最后提醒一下,這題在牛客網同一份代碼有時 \(\mathrm{MLE}\) 最后三個點,有時只占用總限制內存大小的一半就 \(\mathrm{AC}\) 了,如果出現這種神奇的情況,多交幾次就過了(可能是評測機異常或者測試數據隨機?我稱之為玄學)。

例題二 求最大公因數

NC26255 小陽的貝殼
  這題要求最大公因數和差分最值,最值上一題已經求過了,這最大公因數怎么維護出來呢?而且修改是區間修改的,這貌似也增加了維護最大公因數的難度。我們分開思考,如果只有 \(1\)\(2\) 兩種操作,區間加和差分是很好維護的,只需要在區間起始位置和終止位置加 \(1\) 處加上對應值即可(例如我們要原數組 \([2,3]\) 區間加上 \(4\) ,首先是要修改差分數組上的 \(t[2] +4\), 然后還要修改 \(t[4]-4\) ,這也是很好理解的,畢竟 \([2,3]\) 區間比其他區間突出了一塊,整體提高了 \(4\) ,而其他的區間的差分關系並沒有被改變)。
  我們接着思考最大公因數的求法,無非就是輾轉相除法和更相減損術,誒,這更相減損術有點差分的意味了。根據更相減損術,有:

\[gcd(a,b) = gcd(a,|b-a|) \]

  我們想辦法讓他和差分聯系起來。設原數組為 \(A\) ,差分數組為 \(T\) ,則有:

\[\begin{aligned} gcd(A_1,A_2,A_3) & = gcd(A_1,gcd(A_2,A_3))\\ &= gcd(A_1,A_2,|A_3-A_2|)\\&=gcd(A_1,|A_2-A_1|,|A_3-A_2|)\\&=gcd(A_1,|T_2|,|T_3|) \end{aligned} \]

  拓展到多個整數的情況,對於區間 \([i,j]\),有:

\[\begin{aligned} gcd(A_i,A_i+1,···,A_j)&=gcd(A_i,|T_{i+1}|,|T_{i+2}|,···,|T_j|) \\&=gcd(\sum_{k=1}^{i}T_k,|T_{i+1}|,|T_{i+2}|,···,|T_j|) \end{aligned} \]

  這樣,我們通過維護差分數組 \(T\)區間和,區間絕對值最大值,區間最大公因數三個信息就可以做出這道題了。有思路的可以開始做了,代碼並不難,只是注意什么值不取絕對值,什么值取絕對值
  我的代碼里有一個求 \(\mathrm{gcd}\) 函數(百度的),如果是自己寫記得要特判可能會除0的情況。還有一個辦法是調用 \(<\mathrm{algorithm}>\) 庫里的 \(\_\_\mathrm{gcd}\) 的函數。

\(Code\):

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
#define mid (l+r)/2
#define ls now<<1
#define rs now<<1|1
const int maxn = 1e5+5;
inline int Gcd(int a,int b){
    if(a == 0) return b;
    if(b == 0) return a;
    while(b^=a^=b^=a%=b);
    return a;
}
int a[maxn],n,m,cha[maxn]; //a為原數組,cha為差分數組
//線段樹節點
struct node{
    int sum,gcd,gap;//區間和,最大公因數,最大差的絕對值
}t[maxn<<2];

void pushup(int now){
    t[now].sum = t[ls].sum + t[rs].sum;      //直接加
    t[now].gcd = Gcd(t[ls].gcd,t[rs].gcd);      //兩邊取最大公約數
    t[now].gap = max(t[ls].gap,t[rs].gap);     //兩邊取最大差
}

void build(int now,int l,int r){
    if(l == r) {
        t[now].sum = cha[l];
        t[now].gcd = abs(cha[l]);     //取絕對值
        t[now].gap = abs(cha[l]);     //取絕對值
        return;
    }
    build(ls,l,mid);
    build(rs,mid+1,r);
    pushup(now);
}

void update(int now,int l,int r,int pos,int value){
    if(l == r) {
        t[now].sum = cha[l];
        t[now].gcd = abs(cha[l]);
        t[now].gap = abs(cha[l]);
        return;
    }
    if(pos <= mid) update(ls,l,mid,pos,value);
    else update(rs,mid+1,r,pos,value);
    pushup(now);
}

int queryGap(int now,int l,int r,int x,int y){
    if(x <= l && r <= y) return t[now].gap;
    int ans = 0;
    if(x <= mid) ans = max(ans,queryGap(ls,l,mid,x,y));
    if(y > mid) ans = max(ans,queryGap(rs,mid+1,r,x,y));
    return ans;
}

int querySum(int now,int l,int r,int x,int y){
    if(x <= l && r <= y) return t[now].sum;
    int ans = 0;
    if(x <= mid) ans += querySum(ls,l,mid,x,y);
    if(y > mid) ans += querySum(rs,mid+1,r,x,y);
    return ans;
}

int queryGcd(int now,int l,int r,int x,int y){
    if(x <= l && r <= y) return t[now].gcd;
    int ans = 0;
    if(x <= mid) ans = Gcd(ans,queryGcd(ls,l,mid,x,y));
    if(y > mid) ans = Gcd(ans,queryGcd(rs,mid+1,r,x,y));
    return ans;
}

int main(){
    speedUp_cin_cout
    cin>>n>>m;
    For(i,1,n) cin>>a[i],cha[i] = a[i] - a[i-1];
    build(1,1,n);
    int op,x,y,v;
    For(i,1,m){
        cin>>op>>x>>y;
        if(op == 1) {
            cin>>v;
            cha[x] += v;   //注意要修改一下查分數組,和我寫法有關
            update(1,1,n,x,v);
            if(y < n) {        //如果不是最后一個還要修改一個單點
                cha[y+1] -= v;
                update(1,1,n,y+1,-v);
            }
        }else if(op == 2) cout<<queryGap(1,1,n,x+1,y)<<endl;  //操作2
        else cout<<Gcd(querySum(1,1,n,1,x),queryGcd(1,1,n,x+1,y))<<endl;  //操作3
    }
    return 0;
}

  希望對你理解有所幫助,如果有不清楚的的地方歡迎和我討論💡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM