區間問題

給你一段長為n的數列,進行一些修改後,詢問問題。

而修改的方式有兩種:單點修改、區間修改

詢問的問題可能如下:

1.詢問這段區間的最大值/最小值、第k大的數字

2.這段區間的和

問題簡介

前綴和

  • 適用情境:求一段區間內的最大/最小值

方法:

將兩塊合併!

 

Sparse table

而我們要紀錄從此點出發,長度為2^n區間中的最大/小值

ex.會有這個陣列

s[i][j]代表以i為起點,i+2^j-1為終點的方塊

void build(int arr[], int n)
    {
        for (int i=0; i<n; i++)
            mn[i][0] = arr[i];
        
        for (int lg = 0; lg+1 <maxlg; lg++)
        {
            int len = 1 << lg; // 2的lg次方
            for (int i=0; i+len < n; i++) //注意 i+len不要超過
                mn[i][lg+1] = min(mn[i][lg], mn[i+len][lg]);
        }
    }

舉例來說,假設我要尋找3-7這個區間的最小值

7-3+1=5

log2 5 的整數部分為2

那麼我會尋找

3-6 和 4-7 這兩塊區間 (長度為2^2=4)

中間會有重複!

int query(int l, int r)
    {
        int lg = std::__lg(r-l+1);//回傳以2為底的對數的整數部分
        int len = 1 << lg;
        return min(mn[l][lg], mn[r+1-len][lg]);
    }

適用情境:單點加值、求前綴和

結構示意圖:

BIT

在介紹BIT之前要先來介紹lowbit是甚麼

定義:

lowbit ( n ) 定義為非負整數 n 在二進制表示下“最低位的 1 及其後面的所有的 0 ” 的二進制的數值。

舉例:

10 表示成二進位為1010       這時會取10   ,十進位後也就是2

44 表示成二進位為101100   這時會取100 ,十進位後就是4

Lowbit

lowbit(x) = x&-x  = x& ( ~x+1) (&為位元運算 = and)

e.g.

x = 7

x = 00111

-x = 11000+1 = 11001

x&-x = 00001 = 1 

Lowbit怎麼取?

再回來看圖

假設我們要求a[1...14]的和

s[14] = a[13]+a[14]                                 lowbit(14) = 2

那a[1...12]呢?

s[12] = a[9]+a[10]+a[11]+a[12]              lowbit(12) = 4

再看 a[1...8]

s[8]就可以得到了!

於是我們可以發現

sum(x) = s[x] + sum( x-lowbit(x) )

那就可以解決了!

int sum(int id)
{
	int res=0;
    for (int i=id; i>0; i -= i&-i)
    	res+=s[i];
       return res;
}

那我們要更新的時候,就把有覆蓋的點更新就好了


s[0] = i, s [k+1] = s[k]+lowbit(sk)

單點加值

void upd(int id, int x)
{
	for (int i=id; i<=n; i+= i&-i)
    	s[i]+=x;
}

Text

範例code

struct BIT {
    int n;
    vector <ll> dat;
    void init(int _n)
    {
        n = _n;
        dat.assign(n+1, 0); 
    }
    
    void upd (int id, ll x)
    {
        for (int i=id; i<=n; i+= i&-i)
            dat[i] += x;
    }
    
    ll sum(int id)
    {
        ll res=0;
        for (int i=id; i>0; i -= i&-i)
            res += dat[i];
        return res;
    }
};
  • 想法類似分治
  • 一直分兩半
  • 而過程用節點紀錄
  • 結構是一個二元樹
  • 作法:可以用指標或者陣列作法

線段樹

  • 找到這個葉節點
  • 然後更新其所有祖先

 

作法

看此點的位置在目前的左半還是右半,然後遞迴,

要記得更新有跑到的區間

單點修改

  • 看要查詢的區間在左邊還是右邊,然後遞迴
  • 如果目前區間完整的被欲查詢區間包含就回傳
  • 如果區間跨越了兩個?
  • 那就兩邊都跑之後再回傳比較

 

 

區間查詢

3,5

3,4

5,5

3,4

5,5

5,5

  • 要求:將一段區間的值上都增加某個值
  • 修改題目提到的區間
  • 還有哪一些需要更新?
  • 要用到他們的時候在更新!

 

懶人標記-區間修改

在3-7區間新增三

之後pull回去

tag=3

tag=3

tag=3

新增3

新增3

新增3

查詢5

tag=3

tag=3

tag=3

tag=3

tag=3

新增3

例題

範例code - init&build

struct Node
{
    Node *lc, *rc;
    int mx, tag; // tag 左右子樹整個子樹需要加上的數值
    void pull () { mx = max(lc->mx, rc->mx); }
}
*root = nullptr; 

Node *build(int arr[], int l, int r)
{
    Node *res = new Node(); 
    if (l==r)
    {
        res->lc = res->rc = nullptr; // 將res.lc, res.rc都設為空
        res->mx = arr[l];
    }
    else
    {
        int m = (l+r) /2;
        res->lc = build(arr, l, m);
        res->rc = build(arr, m+1, r);
        res->pull();
    }
    return res;
}

範例code - modify&query

void modify(Node *nd, int val, int p, int l, int r)
{
    if (l==r)
    {
        nd->mx = val;
        return;
    }
    int m = (l+r)/2;
    
    if (p <= m)
        modify(nd->lc, val, p, l, m);
    else
    modify(nd->rc, val, p, m+1, r);
    nd->pull(); // Important!!
}

int query(Node *nd, int ql, int qr, int l, int r)
{
    if (r <ql || l > qr)
        return -inf;
    if (ql <=l && r <=qr)
        return nd->mx;
        int m = (l+r)/2;
        return max( query(nd->lc, ql, qr, l, m), query(nd->rc, ql, qr, m+1, r) );
}

範例code - modify&query(懶標)

void modify(Node *nd, int ql, int qr, int d, int l, int r)
{
    if (r<ql || l>qr) return;
    if (ql <= l && r <= qr)
    {
    	nd->mx += d;
        nd->tag += d;
        return;
    }
    push(nd, l, r); // 將tag往下推
    int m = (l+r)/2;
    modify(nd->lc, ql, qr, d, l, m);
    modify(nd->rc, ql, qr, d, m+1, r);
    nd->pull();
}

int query(Node *nd, int ql, int qr, int l, int r)
{
    if (r <ql || l > qr)
        return -inf;
    if (ql <=l && r <=qr)
        return nd->mx;
    push(nd, l, r); // 將tag往下推
    int m = (l+r)/2;
    return max( query(nd->lc, ql, qr, l, m), query(nd->rc, ql, qr, m+1, r) );
}

範例code - push(懶標)

void push(Node *nd, int l, int r)
{
	if (l==r) nd->tag = 0;
    if (!nd->tag) return;
    nd->lc->tag += nd->tag; //合併其原本就有的tag
    nd->lc->mx += nd->tag;
    nd->rc->tag += nd->tag;
    nd->rc->mx += nd->tag;
    nd->tag=0;// 推下去了,那自己的tag就歸零
}

題目

Desk

By yujuan

Desk

  • 268