分治

謝允恆

講師介紹

  • 218謝允恆
  • 不是資訊社
  • APCS考到了4/5 所以沒有基礎觀念

What is 分治?

分治

分而治之

  • :切割問題
  • :解決問題

分治

  • 切割問題(分)

    • 遞迴分兩半求解
    • 切割到規模夠小解決
  • 解決問題(治)

    • 切割到規模夠小時會有一定的特性能解決子問題

  • 合併問題

    • 觀察問題的性質

    • 將子問題的答案回推至大問題

"分"治 be like

以求出最大值為例

1 7 5 6 2 3 8 4
1 7 5 6
2 3 8 4
1 7
5 6
2 3
8 4

1

7

5

6

2

3

8

4

一個數裡的最大值肯定是自己

分"治" be like

以求出最大值為例

1

7

5

6

2

3

8

4

7

6

3

8

7

8

8

好耶答案

(不過誰會拿基礎的分治來求最大值)

實作

#include<iostream>
using namespace std;
const int N=5e5+30;
int a[N];
int DnC(int l,int r){
    if(l==r) return a[l];
    int mid=(l+r)/2;
    return max(DnC(l,mid),DnC(mid+1,r));
}
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
    	cin>>a[i];
    }
    cout<<DnC(0,n-1)<<"\n";
} 

左閉右閉

何時要用分治

使用時機

  1. 大問題不好解,但切割到很小時我們會解
  2. 可以分割成幾個互不影響的小問題
  3. 能遞迴的東西
  4. 之後教的東西都有分治的想法(簡報最後會介紹)
  5. 你在哪,分治就在哪

合併排序

Merge sort

排序

  • 你 \(n\) 小到大排好
  • 希望在 \(O(n\ log\ n)\) 完成
  • 合併排序可使用分治法實作

合併排序

  • 分:
    • 把序列切一半
    • 持續遞迴
  • 治:
    • 只有一個元素的序列是排好的
  • 合:
    • 兩個排好的序列合併成一個

欸不對,怎麼合併

如果已經排好的話...

1 5 6 7
2 3 4 8

A

B

1

2

3

4

5

6

7

8

一層複雜度:\(O(n)\)

1 7 5 6 2 3 8 4
1 7 5 6
2 3 8 4
1 7
5 6
2 3
8 4

1

7

5

6

2

3

8

4

看看整顆遞迴樹

跟前面那張 "分"治 be like 那張一模一樣

看看整顆遞迴樹

1

7

5

6

2

3

8

4

1 7
5 6
2 3
4 8
1 5 6 7
2 3 4 8
1 2 3 4 5 6 7 8

做一層總共有\(n\)個數字

一層的複雜度:\(O(n)\)

共有\(log\ n\)

總複雜度\(O(n\ log\ n)\)

自己試試看ㄅ

附上講師的code

中間點一下才會出現

#include<iostream>
using namespace std;
const int N=1e5+30;
int a[N],b[N];
void merge_sort(int l,int r){
    if(l==r) return;
    int mid=l+(r-l)/2;
    merge_sort(l,mid);
    merge_sort(mid+1,r);
    for(int i=l;i<=r;i++) b[i]=a[i];
    int x=l,y=mid+1,cur=l;
    while(x<=mid&&y<=r){
    	if(b[x]<=b[y]){
            a[cur]=b[x];
            x++;
        }else{
            a[cur]=b[y];
            y++;
        }
        cur++;
    }
    while(x<=mid){
    	a[cur]=b[x];
        cur++,x++;
    }
    while(y<=r){
    	a[cur]=b[y];
        cur++,y++;
    }
    return;
}
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];
    merge_sort(0,n-1);
    for(int i=0;i<n;i++) cout<<a[i]<<" ";
    cout<<"\n";
}

神奇的函式

#include<algorithm>
merge(a,a+n,b,b+m,c)

把 a 陣列和 b 陣列的東西排好丟到 c 陣列

逆序數對

給你 \(n\) 個數字 \(a_1,a_2,...a_n\)

求出有幾個 \(i\ <\ j\) 符合 \(\ a_i>\ a_j\)

(有多筆測試資料,以 \(n=0\) 結束)

Sample input:

Sample output:

5
1 2 3 4 5
5
1 2 3 5 4
0

Case #1: 0

Case #2: 1

枚舉?

  • 枚舉所有數 \(a_i\)
  • 找出 \(\forall\ j>i\ ,\ a_j<a_i\)
  • \(O(n^2)\)
  • \(n=1000\)  AC
  • \(n=10^5\)    TLE

還是...剛剛講的 merge sort?

  • 想想看排序過的序列,左半的任何一個數字都會比右半全部的元素來的小
  • 所以 merge sort 的過程取到右邊代表...

目前還沒用到的左半元素會和此數形成逆序數對

那要對 merge sort 做...?

  • merge sort 的過程取到右邊代表目前還沒用到的左半元素會和此數形成逆序數對
  • 取到右邊時答案就會增加左邊的剩餘元素數量
  • 答案 = 左邊的數量 + 右邊的數量 + 過程中增加的數量
  • 複雜度: \(O(n\ log\ n)\)
  • \(n=10^5\) AC

實作時間

#include<iostream>
#define int long long
using namespace std;
const int N=1e5+30;
int a[N],b[N];
int reversed(int l,int r){
    if(l==r) return 0;
    int mid=l+(r-l)/2,ans=0;
    ans+=reversed(l,mid);
    ans+=reversed(mid+1,r);
    for(int i=l;i<=r;i++) b[i]=a[i];
    int x=l,y=mid+1,cur=l;
    while(x<=mid&&y<=r){
    	if(b[x]<=b[y]) a[cur++]=b[x++];
        else a[cur++]=b[y++],ans+=(mid-x+1);
    }
    while(x<=mid) a[cur++]=b[x++];
    while(y<=r) a[cur++]=b[y++];
    return ans;
}
signed main(){
	int n,t=0;
    while(cin>>n&&n){
    	for(int i=0;i<n;i++) cin>>a[i];
    	cout<<"Case #"<<++t<<": "<<reversed(0,n-1)<<"\n";
    }
}

這題要 long long

主定理

我不會 QQ

主定理

將一數定為\(T(n)\)表示成:

\(T(n) = aT(\frac{n}{b}) + O(n^d)\)

可以得知​

  • \(d < \log_ba ,則\ T(n)\in O(n^{\log_ba})\)
  • \(d = \log_ba,則\ T(n)\in \Theta(n^d \log n)\)
  • \(d > \log_ba,則\ T(n)\in \Theta(n^d)\)

講中文?

\(就是d和\log_ba比大小,一樣大加log,否則取大的\)

證明(毒)

平面最近點對

平面上給你 \(n\) 個點

求任兩點間最短距離平方

\(n<2\times10^5\ \ \ |x_i|,|y_i|<10^9\)

此處距離定義:

令兩點在 \((x_1,y_1),(x_2,y_2) \),\(距離^2=(x_2-x_1)^2+(y_2-y_1)^2 \)

枚舉?

\(O(n^2)\) 應該不是拿來對付 \(n=2\times10^5\) 的好工具

  • 分:
    • 把平面切左右兩半
    • 持續遞迴
  • 治:
    • 只有一個點時距離為0,但不能被算入答案
  • 合:
    • 已知左右側最近點對,求全部最近點對

平面最近點對

\(d_1\)

\(d_2\)

\(d\)

\(d=min(d_1, d_2)\)

\(d\)

已知左右兩半最近點對

答案會是 \(min(左, 右, 左右之間)\)

問:有沒有一種可能,全部的點都在範圍內,複雜度變回 \(O(n^2)\)

可以注意到:

\(d\)

\(d\)

\(d\)

\(d\)

對於一個點,最多只有8個點在\(y \pm d\)的範圍內

中間符合標準的點排序求出來就好

有興趣看證明的看這裡

複雜度

\(T(n) = 2T(\frac{n}{2}) + O(n \log n)\)

\(\Rightarrow O(n \log^2 n)\)

實作時間

#include<iostream>
#include<algorithm>
#include<vector>
#define int long long
using namespace std;
const int N=2e5+30;
typedef pair<int,int> p;
p P[N];
bool cmp(int a,int b){
    return P[a].second<P[b].second;
}
long long DQ(int l,int r){
    if(l==r) return 8e18;
    int mid=l+(r-l)/2,cur=min(DQ(l,mid),DQ(mid+1,r));
    vector<int> V;
    for(int i=l;i<=r;i++) if(abs(P[i].first-P[mid].first)*abs(P[i].first-P[mid].first)<cur) V.push_back(i);
    sort(V.begin(),V.end(),cmp);
    for(int i=0;i<V.size();i++){
        for(int j=max(0LL,i-10);j<i;j++){
            int x=(P[V[i]].first-P[V[j]].first),y=(P[V[i]].second-P[V[j]].second);
            if(cur>x*x+y*y) cur=x*x+y*y;
        }
    }
    return cur;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>P[i].first>>P[i].second;
    sort(P,P+n);
    cout<<DQ(0,n-1)<<"\n";
}

還能更快嗎? (毒)

可以利用merge sort的想法壓掉後面的 \(log\ n\)

程式碼觀賞時間

#include<iostream>
#include<algorithm>
#include<vector>
#define int long long
using namespace std;
const int N=2e5+30;
typedef pair<int,int> p;
p P[N];
bool cmp(p a,p b){
    return a.second<b.second;
}
long long DQ(int l,int r){
    if(l==r) return 8e18;
    int mid=l+(r-l)/2,midx=P[mid].first,cur=min(DQ(l,mid),DQ(mid+1,r));
    vector<p> W(r-l+5);
    merge(P+l,P+mid+1,P+mid+1,P+r+1,W.begin(),cmp);
    for(int i=l;i<=r;i++) P[i]=W[i-l];
    vector<int> V;
    for(int i=l;i<=r;i++) if(abs(P[i].first-midx)*abs(P[i].first-midx)<cur) V.push_back(i);
    for(int i=0;i<V.size();i++){
        for(int j=max(0LL,i-10);j<i;j++){
            int x=(P[V[i]].first-P[V[j]].first),y=(P[V[i]].second-P[V[j]].second);
            cur=min(cur,x*x+y*y); 
        }
    }
    return cur;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>P[i].first>>P[i].second;
    sort(P,P+n);
    cout<<DQ(0,n-1)<<"\n";
}

快速冪

求 \(a^k\)

\(pow(a,k)\) ? 

小數型態,有誤差

直接跑?

#include<iostream>
using namespace std;
int main(){
	int a,k,ans=1;
	cin>>n>>k;
	for(int i=0;i<k;i++) n*=a;
	cout<<ans<<"\n";
}

複雜度: \(O(n)\)

有點慢

數學課時間

\(a^n \times\ a^m= a^{n+m}\)

所以當 \(k\) 為偶數時 \(a^k=a^{\frac{k}{2}}\times a^{\frac{k}{2}}\)

但 \(k\) 是奇數時 \(a^k=a \times a^{k-1}\) 就好

複雜度分析

\(T(n) = T(\frac{n}{2}) + O(1)\)

\(\Rightarrow O(\log n)\)

最壞的狀況:偶數砍半後都是奇數,做 \(2\times \ log\ n\) 次

複雜度:\(O(log\ n)\)

實作時間

以下提供兩種做法,請點空白處

#include<iostream>
using namespace std;
int way1(int a,int k){
    if(k==0) return 1;
    if(k%2) return a*way1(a,k-1);
    int SQRT=way1(a,k/2);
    return SQRT*SQRT;
}
int way2(int a,int k){
    int ans=1,cur=a;
    while(k){
        if(k%2==1) ans*=cur;
        cur*=cur;
        k/=2;
    }
    return ans;
}
int main(){
    int a,k;
    cin>>a>>k;
    cout<<way1(a,k)<<" "<<way2(a,k)<<"\n";
}

題目會要求和特定數取模要注意

實作時間

#include<iostream>
#define int long long
using namespace std;
const int M=1e9+7;
int dnc(int a,int k){
    if(k==0) return 1;
    if(k%2) return (a*dnc(a,k-1)%M);
    int SQRT=dnc(a,k/2);
    return (SQRT*SQRT)%M;
}
signed main(){
    int t,a,k;
    cin>>t;
    while(t--){
    	cin>>a>>k;
 		cout<<dnc(a,k)<<"\n";
	}
}

矩陣快速冪(毒)

(之後dp優化會再提到)

5 6
7 8
1 2
3 4

B矩陣

19 22
43 50

A矩陣

A \times B\ =

注意:A 矩陣的 column 數要等於 B 矩陣的 row 數

\(1\times 5 + 2\times 7\)

\(1\times 6 + 2\times 8\)

\(3\times 5 + 4\times 7\)

\(3\times 6 + 4\times 8\)

那 \(A^0\) 是......?

a b c
d e f
g h i
1 0 0
0 1 0
0 0 1
a b c
d e f
g h i

?

\left[ \begin{matrix} x_n & x_{n+1} \end{matrix} \right] \times \left[ \begin{matrix} 0 & a \\ 1 & b \\ \end{matrix} \right] \ = \ \left[ \begin{matrix} x_{n+1} & ax_n+bx_{n+1} \\ \end{matrix} \right]
\left[ \begin{matrix} x_1 & x_2 \end{matrix} \right] \times {\left[ \begin{matrix} 0 & a \\ 1 & b \\ \end{matrix} \right]}^n = {\left[ \begin{matrix} x_{n-1} & x_n \\ \end{matrix} \right]}
#include<iostream>
using namespace std;
long long squ[2][2],cur[2][2]={{1,0},{0,1}};
long long MOD(long long x){
    long long M=1e9+7;
    return (x%M+M)%M;
}
void POW(int x){
    long long NEW[2][2],now=x;
    while(now){
        if(now%2){
            NEW[0][0]=MOD(MOD(cur[0][0]*squ[0][0])+MOD(cur[0][1]*squ[1][0]));
            NEW[0][1]=MOD(MOD(cur[0][0]*squ[0][1])+MOD(cur[0][1]*squ[1][1]));
            NEW[1][0]=MOD(MOD(cur[1][0]*squ[0][0])+MOD(cur[1][1]*squ[1][0]));
            NEW[1][1]=MOD(MOD(cur[1][0]*squ[0][1])+MOD(cur[1][1]*squ[1][1]));
            cur[0][0]=NEW[0][0],cur[0][1]=NEW[0][1],cur[1][0]=NEW[1][0],cur[1][1]=NEW[1][1];
        }
        NEW[0][0]=MOD(MOD(squ[0][0]*squ[0][0])+MOD(squ[0][1]*squ[1][0]));
        NEW[0][1]=MOD(MOD(squ[0][0]*squ[0][1])+MOD(squ[0][1]*squ[1][1]));
        NEW[1][0]=MOD(MOD(squ[1][0]*squ[0][0])+MOD(squ[1][1]*squ[1][0]));
        NEW[1][1]=MOD(MOD(squ[1][0]*squ[0][1])+MOD(squ[1][1]*squ[1][1]));
        squ[0][0]=NEW[0][0],squ[0][1]=NEW[0][1],squ[1][0]=NEW[1][0],squ[1][1]=NEW[1][1];
        now/=2;
    }
}
int main(){
    long long x1,x2,a,b,n;
    cin>>x1>>x2>>a>>b>>n;
    squ[0][0]=b,squ[0][1]=a,squ[1][0]=1,squ[1][1]=0;
    if(n==1) cout<<x1<<"\n";
    else if(n==2) cout<<x2<<"\n";
    else{
        POW(n-2);
        cout<<MOD(MOD(cur[0][0]*x2)+MOD(cur[0][1]*x1))<<"\n";
    } 
} 

實作時間

分治用途

  • 線段樹
  • BIT
  • LCA, Sparse Table
  • Karatsuba 多項式乘法
  • Strassen 矩陣乘法
  • FFT
  • 整體二分搜
  • CDQ分治
  • 重心剖分
  • 你在哪,分治就在哪

習題

neoj 124

neoj 788

neoj 789

CF 1131 (這題好像是資結)

CSES 1712

分治

By doubledown13

分治

  • 348