資料結構

Data structure

資料結構

電腦中儲存、管理、組織資料的方式

不同的組織結構

在適當的時機使用可以加快算法的效率

資料結構

例如:

BFS的時候使用queue

Dijkstra 演算法使用 min heap

其實你們已經很會用了

Set Map 陣列

INDEX

  • 前綴和
  • 差分
  • 樹狀數組
  • 基礎線段樹
  • 懶標線段樹

前綴和

前綴和(Prefix Sum)

今天有一個陣列

求l到r的和

ex l=1 r=4->sum=20+30+40+50=140

前綴和(Prefix Sum)

從左到右 l到r掃過去加起來?

TLE

O(nq)

用前綴和!

前綴和(Prefix Sum)

觀察一下會發現

從l加到r可以這樣算

從1加到r-從1加到l-1

那要怎麼快速處理?

 

前綴和(Prefix Sum)

我們定義一個陣列pre

算出來整個陣列後

求區間和變得超簡單

sum(l,r)=pre_r-pre_{l-1}

單次詢問時間變成:

O(1)

前綴和(Prefix Sum)

要怎麼實作?

pre_i=pre_{i-1}+a_i

預處理複雜度:

總時間複雜度:

O(n)
O(n+q)

前綴和(Prefix Sum)

#include<bits/stdc++.h>
using namespace std;
#define maxn 200005

int n,q,arr[maxn],pre[maxn];

main(){
    cin>>n>>q;
    for(int i=1;i<=n;++i) cin>>arr[i];
    for(int i=1;i<=n;++i) pre[i] = pre[i-1]+arr[i];

    for(int i=1;i<=q;++i){
        int l,r; cin>>l>>r;
        cout<<pre[r]-pre[l-1]<<endl;
    }
}

前綴和(Prefix Sum)

前面應該只是開胃菜

如果陣列變二維的話呢?

前綴和(Prefix Sum)

前綴和(Prefix Sum)

跟一維的有點像 但定義有點不同

我們定義                為包含這格 左上全部加起來

pre_{ij}

(i,j)

前綴和(Prefix Sum)

那要怎麼求出矩形內的和呢?

求橘色矩形中的和

A

-C

-B

+D

前綴和(Prefix Sum)

前綴和(Prefix Sum)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 1e18
#define maxn 505

int arr[maxn][maxn],n,m,q,pre[maxn][maxn];

int query(int a,int b,int x,int y){
    return pre[x][y] + pre[a-1][b-1] - pre[x][b-1] - pre[a-1][y];
}
main(){
    cin>>n>>m>>q;
    for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) cin>>arr[i][j];
    for(int i=1;i<=n;++i) for(int j=1;j<=m;++j)
        pre[i][j] = pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+arr[i][j];
    while(q--){
        int a,b,x,y; cin>>a>>b>>x>>y;
        a++,b++,x++,y++;
        cout<<query(a,b,x,y)<<endl;
    }
}

差分

差分(Difference)

今天有一個陣列

要將l~r都加上x

直接爆

從l掃到r

每格+=x

O(nq)

->TLE

差分

我們定義一個新的陣列

支援快速修改區間

定義:

D_i=a_i-a_{i-1}

也就是紀錄相鄰兩項的差

簡單來說 差分就是相鄰兩元素的差值

差分

我們可以發現

a_i=\sum_{j=1}^{i} D_j

可以想成第一項加上每一次的變化量

就可以還原陣列

差分

接著如果要修改整的區間

我們會發現超簡單

當我們在還原的時候

就可以發現

區間[l,r]都被加x了

D_l+x\\ D_r+1-x

差分

那把差分陣列用前綴和存就會發現變成原陣列了

總時間複雜度:

O(q+n)
a_i=\sum_{j=1}^{i} D_j

差分

範例code

#include<bits/stdc++.h>
using namespace std;
#define maxn 200005

int n,q,arr[maxn],D[maxn],pre[maxn];

main(){
    cin>>n>>q;
    for(int i=1;i<=n;++i) cin>>arr[i];
    for(int i=1;i<=n;++i) D[i] = arr[i]-arr[i-1];
    for(int i=1;i<=q;++i){
        int l,r,x; cin>>l>>r>>x;
        D[l] += x;
        D[r+1] -= x;
    }
    for(int i=1;i<=n;++i) pre[i] = pre[i-1]+D[i];
    for(int i=1;i<=n;++i) cout<<pre[i]<<' ';
    cout<<endl;
}

差分

#include<bits/stdc++.h>
using namespace std;
int n,as,t;
vector<pair<int,int>> pv;
int main(){
    cin>>n;
    for(int i=0;i<n;++i){
        int a,b; cin>>a>>b;
        pv.push_back({a,1});
        pv.push_back({b+1,-1});
    }
    sort(pv.begin(),pv.end());
    for(auto it:pv){
        t += it.second;
        as = max(as,t);
    }
    cout<<as;
    return 0;
}

樹狀樹組

樹狀樹組(BIT)

當我們發現前綴和序列不能做修改

每次改都是O(n)

於是就要用到樹狀樹組!

(二元索引樹)

(Fenwick tree)

(Binary Index Tree)

可以做到

單點修改,區間查詢

區間修改,單點查詢(套差分)

lowbit

lowbit(x) 代表 x 的二進位表示法中

最小為 1 的位數所代表的2的冪次

ex:

lowbit(12)=4  ->12的二進位=1100

lowbit(16)=16  ->16的二進位=10000

lowbit

如何快速計算 lowbit ?

非常簡單,lowbit(x) = x & (-x)

為什麼 ?

 x = 00010100

 -x =  11101011 + 00000001

= 11101100

x&(-x) = 00000100 

lowbit

int lowbit(int x){
    return x&(-x);
}

Code

超簡單

BIT

查詢前綴和

15  -> 1111 -> lowbit = 1

14 -> 1110 -> lowbit = 2

12 -> 1100 -> lowbit = 4

8 -> 1000 -> lowbit = 8

前15項的和:

bit_{15}+bit_{14}+bit_{12}+bit_{8}

查詢前綴和

11  -> 1011 -> lowbit = 1

10 -> 1010 -> lowbit = 2

8 -> 1000 -> lowbit = 8

前11項的和:

bit_{11}+bit_{10}+bit_{8}

查詢前綴和

int query(int x){
    int cnt=0;
    while (x > 0){
        cnt += bit[x];
        x -= lowbit(x);
    }

    return cnt;
}

Code

最小節點:1(不會<1)

單點修改

單點修改

9  ->  01001 -> lowbit = 1

10 -> 01010 -> lowbit = 2

12 -> 01100 -> lowbit = 4

16 -> 10000 -> lowbit = 16

若要加值 9 

則我們要將 bit 的 9、10、12、16

全部都加值

單點修改

void modify(int x, int v){
    while (x <= n){
        bit[x] += v;
        x += lowbit(x);
    }
}

Code

最大節點:n 

總結

可以發現

單點修改、查詢前綴和會用到的bit位置數量

都是O(log n)

建構BIT

把每一個元素當成在修改

O(n log n)

總結

超好寫

比下禮拜要教的線段樹好寫多了

如果要支援

區間改值、單點查詢也很簡單

只需要用 bit 的原本功能 (單點改、查前綴和)

再套上差分就可以做到

模板

int lowbit(int x){
    return x&(-x);
}

int query(int x){
    int cnt=0;
    while (x > 0){
        cnt += bit[x];
        x -= lowbit(x);
    }

    return cnt;
}

void modify(int x, int v){
    while (x <= n){
        bit[x] += v;
        x += lowbit(x);
    }
}

題目

CODE

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 200005
#define int long long
using namespace std;

int n, q, bit[maxn];

int lowbit(int x){
    return x&(-x);
}

int query(int x){
    int cnt=0;
    while (x > 0){
        cnt += bit[x];
        x -= lowbit(x);
    }

    return cnt;
}

void modify(int x, int v){
    while (x <= n){
        bit[x] += v;
        x += lowbit(x);
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> q;
    for (int i=1; i<=n; ++i){
        int k; cin >> k;
        modify(i, k);
    }
    for (int i=1; i<=q; ++i){
        int a, b, c; cin >> a >> b >> c;
        if (a==1){
            modify(b, c-(query(b)-query(b-1)));
        }else{
            cout << query(c) - query(b-1) << endl;
        }
    }
}

基礎線段樹

線段樹

Segment Tree

處理區間問題的強大工具

線段樹是什麼?可以吃嗎?

支援各種區間查詢

能在                     內做出各種區間操作 

O(log\ n)

線段樹

核心概念就是將資料分段

再將不同片段合起來求出答案

以區間和為例

線段樹

每一個父節點都是由兩個子節點推得

形成一個二元樹

有分兩種寫法:

迭代式線段樹

遞迴式線段樹

這邊教遞迴式

個人覺得比較好理解

也比較好寫(?

線段樹

用這樣的方式比較好看

每一個節點都對到一個區間

假如我們今天要查詢1~6區間

線段樹

陣列格子裡面放的都是已經算完的答案

所以可以大大節省時間

O(log\ n)

每次查詢複雜度:

線段樹

設定每個點的父節點tree[k]

左子節點:tree[k*2] 右子節點:tree[k*2+1]

實作

一顆基本的線段樹需要:

  • build() 建構
  • query() 區間查詢
  • modify() 單點修改值

實作

前情提要

接下來的示範都會用1-based

比較好寫 有時候速度也比較快

大小為n的陣列arr[n]

開的tree陣列大小是4*n

原陣列arr[n] 線段樹陣列tree[n]

#include<bits/stdc++.h>
#define maxn 200005
using namespace std;
int arr[maxn],tree[4*maxn],n;

Build

inline void pull(int ls,int rs,int x){
    tree[x]=tree[ls]+tree[rs];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}

pull:將兩個子節點的值做運算

推算父節點

到葉節點時 tree[x]=arr[r]=arr[l]

遞迴往下做兩個子節點的值

ls:左子節點 rs:右

兩子節點遞迴完再pull

Query

int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=0;
    if(mid>=a)res+=query(a,b,l,mid,ls);
    if(mid+1<=b)res+=query(a,b,mid+1,r,rs);
    return res;
}

當現在tree[x]的範圍都包含在查詢區間中

直接return回傳

目標左界有包含在左子節點範圍

右界右有包含在右子節點範圍

Modify

void modify(int a,int l,int r,int x,int v){
    if(l==r){
        tree[x]=v;
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,l,mid,ls,v);
    else modify(a,mid+1,r,rs,v);
    pull(ls,rs,x);
}

到達要修改的葉節點

->直接修改 return

往下找

要先判斷往左還是右走

(縮小範圍)

inline void pull(int ls,int rs,int x){
    tree[x]=tree[ls]+tree[rs];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=0;
    if(mid>=a)res+=query(a,b,l,mid,ls);
    if(mid+1<=b)res+=query(a,b,mid+1,r,rs);
    return res;
}
void modify(int a,int l,int r,int x,int v){
    if(l==r){
        tree[x]=v;
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,l,mid,ls,v);
    else modify(a,mid+1,r,rs,v);
    pull(ls,rs,x);
}

CODE

#include<bits/stdc++.h>
#define maxn 200005
#define int long long
using namespace std;
int n,arr[maxn],tree[4*maxn];
inline void pull(int ls,int rs,int x){
    tree[x]=tree[ls]+tree[rs];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=0;
    if(mid>=a)res+=query(a,b,l,mid,ls);
    if(mid+1<=b)res+=query(a,b,mid+1,r,rs);
    return res;
}
void modify(int a,int l,int r,int x,int v){
    if(l==r){
        tree[x]=v;
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,l,mid,ls,v);
    else modify(a,mid+1,r,rs,v);
    pull(ls,rs,x);
}
signed main(){
    int q;
    cin>>n>>q;
    for(int i=1;i<=n;i++){
        cin>>arr[i];
    }
    build(1,n,1);
    for(int i=1;i<=q;i++){
        int t;cin>>t;
        int a,b;cin>>a>>b;
        if(t==1){
            modify(a,1,n,1,b);
        }
        else{
            cout<<query(a,b,1,n,1)<<'\n';
        }
    }
}
#include<bits/stdc++.h>
#define maxn 200005
#define int long long
using namespace std;
int n,arr[maxn],tree[4*maxn];
inline void pull(int ls,int rs,int x){
    tree[x]=min(tree[ls],tree[rs]);
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=1e9;
    if(mid>=a)res=min(res,query(a,b,l,mid,ls));
    if(mid+1<=b)res=min(res,query(a,b,mid+1,r,rs));
    return res;
}
void modify(int a,int l,int r,int x,int v){
    if(l==r){
        tree[x]=v;
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,l,mid,ls,v);
    else modify(a,mid+1,r,rs,v);
    pull(ls,rs,x);
}
signed main(){
    int q;
    cin>>n>>q;
    for(int i=1;i<=n;i++){
        cin>>arr[i];
    }
    build(1,n,1);
    for(int i=1;i<=q;i++){
        int t;cin>>t;
        int a,b;cin>>a>>b;
        if(t==1){
            modify(a,1,n,1,b);
        }
        else{
            cout<<query(a,b,1,n,1)<<'\n';
        }
    }
}

懶標線段樹

懶標線段樹

修改一個點:

O(log\ n)

區間修改?

最糟:

O(n\ log\ n)

TLE

區間改值

懶人標記

修改區間[1,5]需要修改哪些值

區間改值

在節點2紀錄一個標記:tag

等需要子節點資訊的時候

再往下推

複雜度會降回:

O(log\ n)

實作

pull+build

inline void pull(int l,int r,int x){
    tree[x]=tree[l]+tree[r];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}

跟基礎線段樹一模一樣

實作

void mark(int l,int r,int x,int v){
    tree[x]+=(r-l+1)*v;
    tag[x]+=v;
}
void push(int l,int r,int x){
    if(tag[x]){
        int ls=x*2,rs=ls+1,mid=(l+r)/2;
        mark(l,mid,ls,tag[x]);
        mark(mid+1,r,rs,tag[x]);
        tag[x]=0;
    }
}

mark+push

mark會把這一格改值 他下面的子樹都是未改的

push向下推 把tag推下去後清空

實作

void modify(int a,int b,int l,int r,int x,int v){
    if(l>=a && r<=b){
        mark(l,r,x,v);
        return;
    }
    push(l,r,x);
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,b,l,mid,ls,v);
    if(mid+1<=b)modify(a,b,mid+1,r,rs,v);
    pull(ls,rs,x);
}

modify

修改的時候順便push

修改完再pull

實作

int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=0;
    push(l,r,x);
    if(mid>=a)res+=query(a,b,l,mid,ls);
    if(mid+1<=b)res+=query(a,b,mid+1,r,rs);
    return res;
}

query

查值前push

其他一模一樣

CODE

#include<bits/stdc++.h>
#define maxn 200005
#define int long long
using namespace std;
int n,q,arr[maxn],tag[4*maxn],tree[4*maxn];
inline void pull(int l,int r,int x){
    tree[x]=tree[l]+tree[r];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
void mark(int l,int r,int x,int v){
    tree[x]+=(r-l+1)*v;
    tag[x]+=v;
}
void push(int l,int r,int x){
    if(tag[x]){
        int ls=x*2,rs=ls+1,mid=(l+r)/2;
        mark(l,mid,ls,tag[x]);
        mark(mid+1,r,rs,tag[x]);
        tag[x]=0;
    }
}

題目

zj d799

模板題

要注意範圍->500000

#include<bits/stdc++.h>
#define maxn 500005
#define int long long
using namespace std;
int n,q,arr[maxn],tag[4*maxn],tree[4*maxn];
inline void pull(int l,int r,int x){
    tree[x]=tree[l]+tree[r];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
void mark(int l,int r,int x,int v){
    tree[x]+=(r-l+1)*v;
    tag[x]+=v;
}
void push(int l,int r,int x){
    if(tag[x]){
        int ls=x*2,rs=ls+1,mid=(l+r)/2;
        mark(l,mid,ls,tag[x]);
        mark(mid+1,r,rs,tag[x]);
        tag[x]=0;
    }
}

void modify(int a,int b,int l,int r,int x,int v){
    if(l>=a && r<=b){
        mark(l,r,x,v);
        return;
    }
    push(l,r,x);
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,b,l,mid,ls,v);
    if(mid+1<=b)modify(a,b,mid+1,r,rs,v);
    pull(ls,rs,x);
}
int query(int a,int b,int l,int r,int x){
    if(l>=a && r<=b)return tree[x];
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    int res=0;
    push(l,r,x);
    if(mid>=a)res+=query(a,b,l,mid,ls);
    if(mid+1<=b)res+=query(a,b,mid+1,r,rs);
    return res;
}
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>arr[i];
    build(1,n,1);
    cin>>q;
    for(int i=1;i<=q;i++){
        int t;cin>>t;
        if(t==1){
            int a,b,v;cin>>a>>b>>v;
            modify(a,b,1,n,1,v);
        }
        else{
            int a,b;cin>>a>>b;
            cout<<query(a,b,1,n,1)<<'\n';
        }
    }
}
#include<bits/stdc++.h>
#define maxn 200005
#define int long long
using namespace std;
int n,q,arr[maxn],tag[4*maxn],tree[4*maxn];
inline void pull(int l,int r,int x){
    tree[x]=tree[l]+tree[r];
}
void build(int l,int r,int x){
    if(l==r){
        tree[x]=arr[r];
        return;
    }
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pull(ls,rs,x);
}
void mark(int l,int r,int x,int v){
    tree[x]+=(r-l+1)*v;
    tag[x]+=v;
}
void push(int l,int r,int x){
    if(tag[x]){
        int ls=x*2,rs=ls+1,mid=(l+r)/2;
        mark(l,mid,ls,tag[x]);
        mark(mid+1,r,rs,tag[x]);
        tag[x]=0;
    }
}

void modify(int a,int b,int l,int r,int x,int v){
    if(l>=a && r<=b){
        mark(l,r,x,v);
        return;
    }
    push(l,r,x);
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)modify(a,b,l,mid,ls,v);
    if(mid+1<=b)modify(a,b,mid+1,r,rs,v);
    pull(ls,rs,x);
}
int query(int a,int l,int r,int x){
    if(l==r && r==a)return tree[x];
    push(l,r,x);
    int ls=x*2,rs=ls+1,mid=(l+r)/2;
    if(mid>=a)return query(a,l,mid,ls);
    else return query(a,mid+1,r,rs);
}
signed main(){
    cin>>n>>q;
    for(int i=1;i<=n;i++)cin>>arr[i];
    build(1,n,1);
    for(int i=1;i<=q;i++){
        int t;cin>>t;
        if(t==1){
            int a,b,v;cin>>a>>b>>v;
            modify(a,b,1,n,1,v);
        }
        else{
            int a,b;cin>>a;
            cout<<query(a,1,n,1)<<'\n';
        }
    }
}

單點查詢

Made with Slides.com