Algorithm - 11

演算法之雜七雜八

Lecturer:退幹的高二倖存者,aka Repkironca

CONTENT

01 前綴和

你知道為何我不去畢旅嗎?我也不知道誒:)

01 前綴和

靜態區間求和問題

[翻譯年糕]

你有一個大小為 \(N\) 的陣列 \(A\)

裡面充滿正整數 \(x_1, x_2, ..., x_N\)

\(1 \leq N \leq 2×10^5\),\(1 \leq x_{i} \leq 10^9\)

另有 Q 筆詢問,\(1 \leq Q \leq 2×10^5\)

每筆詢問會求 [l, r] 之間的數字總和

直接做?

01 前綴和

靜態區間求和問題

直接做?

\(O(NQ)\),\(2×10^{14}\),會爛

前綴和:

預先按照順序,把加總算完,之後就可以 O(1) 求出

原陣列 arr 3 2 4 5 1 1 5 3
前綴和 presum

3

5

9

14

15

16

21

24

01 前綴和

靜態區間求和問題

原陣列 arr 3 2 4 5 1 1 5 3
前綴和 presum

3

5

9

14

15

16

21

24

index (1-based) (1) (3) (4) (5) (6) (8)

求 [l, r] 之和:presum[r] - presum[l-1]

e.g. 求 [3, 7] 之總和

(2)

(7)

01 前綴和

靜態區間求和問題

原陣列 arr 3 2 4 5 1 1 5 3
前綴和 presum

3

5

9

14

15

16

21

24

index (1-based) (1) (3) (4) (5) (6) (8)

求 [l, r] 之和:presum[r] - presum[l-1]

e.g. 求 [3, 7] 之總和

(2)

(7)

(7)

\(= (1) + (2) + (3) + (4) + (5) + (6) + (7)\)

01 前綴和

靜態區間求和問題

原陣列 arr 3 2 4 5 1 1 5 3
前綴和 presum

3

5

9

14

15

16

21

24

index (1-based) (1) (3) (4) (5) (6) (8)

求 [l, r] 之和:presum[r] - presum[l-1]

e.g. 求 [3, 7] 之總和

(2)

(7)

(7)

\(= (1) + (2) + (3) + (4) + (5) + (6) + (7)\)

(2)

\(= (1) + (2)\)

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

-

01 前綴和

靜態區間求和問題

限制:僅限 靜態 區間求和

    aka 陣列不被修改、不會增加縮短

時間複雜度?

建表 O(N)

查詢 O(1)

空間複雜度?

就,多一個陣列 O(N)

01 前綴和

食酢

嘿對,就把上面的東西做出來,沒什麼難度

然後記得讀到 EOF,破題目

前綴和的二維擴展,想想看要怎麼把剛剛的概念

使用在二維空間上?

喔對,因為這次我都有 AC

後面會給範扣,有機率掉題解

01 前綴和

ZJ_a693 吞食天地

// 忽略那一堆 default code 的怪模板
#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define pii pair <int, int>
#define ll long long int
#define F first
#define S second
#define pq priority_queue
#define usll unsigned long long int
#define pb push_back
using namespace std;

bool first = true;
void solve (){
  int N, M;
  while (cin >> N >> M){
    vector <usll> vec(N+17, 0);
    vector <usll> presum(N+17, 0);
    for (int i = 1; i <= N; i++){
      cin >> vec[i];
      presum[i] = presum[i-1] + vec[i];
    }
    while (M--){
      int l, r; cin >> l >> r;
      cout << presum[r] - presum[l-1] << '\n';
    }
  }
}

int main (){
  icisc
  solve();
}

AC CODE

01 前綴和

ZJ_a694 吞食天地二

\(presum[i][j] = \Sigma^i_{x=1}\;\Sigma^j_{y=1}\;arr[x][y]\)

翻譯年糕:從左上角開始,把 [i][j] 當矩形的右下角

如何在 O(1) 內做到?

if 你想當正常人

\(presum[i][j] = presum[i-1][j-1]+arr[i-1][j]+arr[i][j-1]+arr[i][j]\)

but 蘇昱亙當時大腦打結,所以他範扣這樣寫

\(presum[i][j] = arr[i][j] + presum[i-1][j] + presum[i][j-1] - presum[i-1][j-1]\)

01 前綴和

ZJ_a694 吞食天地二

arr[sx][sy] 至 arr[ex][ey] 之總和:

\(presum[ex][ey] - presum[sx-1][ey] - presum[ex][sy-1] + presum[sx-1][sy-1]\)

1 2 3 4 5 6
1
2
3
4
5
6

arr[3][3] ~ arr[6][5]

+ arr[6][5]

- arr[2][5]

- arr[6][2]

+ arr[2][2]

01 前綴和

ZJ_a694 吞食天地二

#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define pii pair <int, int>
#define ll long long int
#define F first
#define S second
#define pq priority_queue
#define usll unsigned long long int
#define pb push_back
using namespace std;

void solve (){
  int N, M;
  while (cin >> N >> M){
    vector <vector <int> > graph(N+17, vector <int>(N+17, 0));
    vector <vector <int> > presum = graph;
    for (int i = 1; i <= N; i++) {
      for (int j = 1; j <= N; j++) {
        cin >> graph[i][j];
        presum[i][j] = graph[i][j] + presum[i-1][j] + presum[i][j-1] - presum[i-1][j-1];
      }
    }

    while (M--){
      int sx, sy, ex, ey; cin >> sx >> sy >> ex >> ey;
      cout << presum[ex][ey] - presum[sx-1][ey] - presum[ex][sy-1] + presum[sx-1][sy-1] << '\n';
    }
  }
}

int main (){
  icisc
  solve();
}

AC CODE

02 差分

你知道為何要退幹嗎?我也不知道誒:)

02 差分

好像沒人為這種題型命名


裡面充滿正整數 \(x_1, x_2, ..., x_N\)

\(1 \leq N \leq 2×10^5\),\(1 \leq x_{i} \leq 10^9\)

另有 Q 次操作,\(1 \leq Q \leq 2×10^5\)

每次操作選定 [l, r] 範圍內的所有數字,將其加上 k

求所有操作結束後的陣列長什麼樣子

你有一個大小為 \(N\) 的陣列 \(A\)

對,因為我找不到裸題

所以就直接生出一題來

02 差分

好像沒人為這種題型命名

Brute Force:\(O (NQ)\)

我們需要一種方式,可以在 O(1) 內解決每次操作

差分

  • 其實就,把此項減掉上一項的結果
差分 6 -4 -1 -8 11 -8 6 -8 9
原陣列 6 2 1 -7 4 -4 2 -6 3
前綴和 6 8 9 2 6 2 4 -2 1

02 差分

好像沒人為這種題型命名

差分 predif 6 -4 -1 -8 11 -8 6 -8 9
原陣列 arr 6 2 1 -7 4 -4 2 -6 3
前綴和 presum 6 8 9 2 6 2 4 -2 1
  • 如果把前綴和拿去做差分,就會變回原陣列
  • 如果把差分拿去做前綴和,就會變回原陣列

只要在做操作時,直接去改變差分

就能在 O(1) 完成了!

聽不懂ㄇ?沒關係那我們重來

02 差分

好像沒人為這種題型命名

聽不懂ㄇ?沒關係那我們重來

index 1 2 3 4 5
原陣列 5 4 3 2 1
差分 5 -1 -1 -1 -1
index 1 2 3 4 5
原陣列 5 6 5 4 1
差分 5 1 -1 -1 -3

[2, 4] 增加 2

由於差分都是 A-B 構成

對 [2] 來說,A 增加了 2,其差分也要 + 2

對 [5] 來說,B 增加了 2,其差分也要 - 2

  • [l, r] 增加 k:\(predif[l] + k,predif[r+1] - k\)

02 差分

好像沒人為這種題型命名

限制:陣列不會增長或縮短,query 次數極少

時間複雜度?

操作 O(1)

查詢 O(N)

空間複雜度?

就,多一個陣列 O(N)

02 差分

食酢

論壇翻到的,超級老題

CF 的 Round #169 根本前寒武紀了吧

 

結合一點 Greedy 的概念,應該不難

它不裸,但我都跟你說是差分了:)

 

話說,它會卡 long long 喔

02 差分

食酢

這題要先加入建北電資的 Group ㄛ

如果你還沒加到,在 discord 問一下或去翻簡報

去年把我搞爆的一題

明明是 pA 不知道在兇幾點

蔡孟衡:應該不會有人寫這題吧

毫不意外,只有 Aaw 輕鬆 AC

02 差分

Little Girl and Maximum Sum
#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define ll long long int
#define usll unsigned long long int
#define pii pair <int, int>
#define F first
#define S second
#define pq priority_queue
#define pb push_back
using namespace std;

void solve (){
  int N, M; cin >> N >> M;
  vector <usll> vec(N);
  for (auto &to:vec) cin >> to;
  sort(vec.begin(), vec.end(), greater<int>());

  vector <usll> predif(N, 0); // 計算出現次數
  while (M--){
    int l, r; cin >> l >> r;
    l--;r--; // cuz input is 1-based
    predif[l]++;
    if (r+1 < N) predif[r+1]--;
  }

  vector <usll> tmp = predif; //差分做前綴和來還原
  for (int i = 1; i < N; i++) tmp[i] += tmp[i-1];
  // for (auto to:tmp) cout << to << ' ';
  sort(tmp.begin(), tmp.end(), greater<int>());

  usll count = 0;
  for (int i = 0; i < N; i++) count += vec[i]*tmp[i];
  cout << count << '\n';
}

int main (){
  icisc
  solve();
}

AC CODE

先計算每格出現次數,出現越多次的就乘上越大的

02 差分

鄉下人與等差數列

03 快速冪

你以後還看得到我嗎?我也不知道誒:)

03 快速冪

你會用 C++ 來計算次方嗎?

int power (int num, int pow){
  int ret = 1;
  int (int i = 0; i < pow; i++) ret *= num;
  return ret;
}

Brute Force

O(N)

pow(n, pow);

<math.h> 內建ㄉ

很抱歉,還是 O(N)

看起來沒什麼問題...
但如果我是要計算五百萬次方呢

03 快速冪

沒錯!快要還更快,區區計算太浪費時間了!

\(3^8\),如果用前面的方法線性去做,你應該要乘 7 次...

6561

81

81

\(9\)

\(9\)

\(9\)

\(9\)

\(3\)

\(3\)

\(3\)

\(3\)

\(3\)

\(3\)

\(3\)

\(3\)

事實上,同一層的結合只要做一次就夠了

反正數字都一樣

03 快速冪

  • 當次方是奇數,多乘上一次自己,次方 - 1
  • 當次方是偶數,拆成對等的兩分,次方 / 2
  • 終止條件:當次方 = 0,回傳 1
int bipow(int num, int pow) {
    if (pow == 0) return 1;
    if (pow&1) return num * bipow(num, pow-1);
    int tmp = bipow(num, pow/2);
    return tmp * tmp;
}

做成遞迴後會長這樣

03 快速冪

模板亂砸時間

ll bipow (ll a, ll b, ll m){
  if (b == 0) return 1;
  if (b&1) return Mult(a, bipow(a, b-1, m), m);
  ll tmp = bipow(a, b/2, m);
  return Mult(tmp, tmp, m);
}

我自己用ㄉ,遞迴版,你看不懂因為其它函式我沒貼上來

long long fastpow(int a,int n){
    if(n==0) return 1;//a^0 = 1
    int half = fastpow(a,n/2);//算出 a^(b/2)
    if(n&1){//n是奇數
        return half*half*a;
    } else {
        return half*half;
    }
}

遞迴白話文版,by yeedrag

int exp(int g, int x, int p) {
    int r, c = g % p;
    for (r = 1; x > 0; x >>= 1) {
        if (x & 1) {
            r = (r * c) % p;
        }
        c = (c * c) % p;
    }
    return r;
}

迭代文言文版,by yennnn

03 快速冪

複雜度?

(如果你很無聊,可以用分治的主定理去求)

不斷把數字 一分為二

\(O(log N)\)

BTW 拜託別這樣寫,超級小丑,這樣還是 \(O(N)\)

int bipow(int num, int pow) {
    if (pow == 0) return 1;
    if (num&1) return num * bipow(num, pow-1);
    return bipow(num, pow/2) * bipow(num, pow/2);
}

03 快速冪

食酢

裸ㄉ,你甚至可以直接複製上面的模板

裸ㄉ,你甚至可以直接複製上面的模板

老實說我覺得超難,我自己也卡很久

可以給你個概念... AC 扣頗簡短

然後相信自己的直覺,往那個方向走

03 快速冪

The last digit

#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define ll long long int
using namespace std;

ll Mod (ll n, ll m){
  return (n+m) % m;
}

ll Mult (ll a, ll b, ll m){
  return Mod(Mod(a, m) * Mod(b, m), m);
}

ll bipow (ll a, ll b, ll m){
  if (b == 0) return 1;
  if (b == 1) return a;
  if (b&1) return Mult(a, bipow(a, b-1, m), m);
  ll tmp = bipow(a, b/2, m);
  return Mult(tmp, tmp, m);
}

void solve (){
  int N; cin >> N;
  while (N--){
    int a, b; cin >> a >> b;
    cout << bipow(a, b, 10) << '\n';
  }
}

int main (){
  icisc
  solve();
}

AC CODE

取最後一位...對,那就模 10

03 快速冪

快速冪

#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define ll long long int
#define usll unsigned long long int
#define pii pair <int, int>
#define F first
#define S second
#define pq priority_queue
#define pb push_back
using namespace std;

ll Mod (ll n, ll m){
  return (n+m) % m;
}

ll Mult (ll a, ll b, ll m){
  return Mod(Mod(a, m) * Mod(b, m), m);
}

ll bipow (ll a, ll b, ll m){
  if (b == 0) return 1;
  if (b&1) return Mult(a, bipow(a, b-1, m), m);
  ll tmp = bipow(a, b/2, m);
  return Mult(tmp, tmp, m);
}

void solve (){
  ll x, y, p; cin >> x >> y >> p;
  cout << bipow(x, y, p) << '\n';
}

int main (){
  icisc
  solve();
}

AC CODE

為何那麼冗長?因為我懶得幫你過濾掉沒用的 default code

03 快速冪

Magic of the locker

先來推估看看正解複雜度...

這次 T、N 乘起來後高達 \(10^{17}\)

由此可見 \(O(TN)\) 會被卡掉,解答不是 O(T) 就是 O(log(TN))

接著我們來嘗試分解 2~5 這幾個比較小的數字...

2:\(1×2=2\) 是最佳解

3:\(1×3=3\) 是最佳解

4:\(2×2=4\) 是最佳解

5:\(2×3=6\) 是最佳解

發現什麼了嗎?由於乘上 1 沒意義,2 和 3 是完全無法再分解ㄉ

03 快速冪

Magic of the locker

另外,所有數字最終都一定可以被分解為 2 或 3

如果給你選的話,2 與 3 你要拿哪一個?

\begin{aligned} 6 => 2×2×2= 8 \end{aligned}
\begin{aligned} 6 => 3×3=9 \end{aligned}

當然是 3 啊廢話,2個 3 的效力大於 3個 2

因此,我們要盡可能拿到最多 3

算出來的值就是 3 的 (n/3) 次方

03 快速冪

Magic of the locker

不過還有個小細節,餘數處理的部分

  • 若 n%3 = 0,當然沒什麼爭議,直接輸出 \(3^{(n/3)}\)

  • 若 n%3 = 2,只要乘上餘數就好,輸出 \(2×3^{((n-2)/3)}\)

但如果 n%3 = 1 呢?

  • 前面說過乘上 1 沒什麼意義,所以我們要把一個 3 退回去輸出 \(2^2×3^{((n-4)/3)}\)

話說,最後記得 1~3 要特判,否則會ㄘ RE

03 快速冪

Magic of the locker

#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define ll long long int
using namespace std;

const int MOD = 1e9 + 7;

ll Mod (ll n, ll m){
  return (n+m) % m;
}

ll Mult (ll a, ll b, ll m){
  return Mod(Mod(a, m) * Mod(b, m), m);
}

ll bipow (ll a, ll b, ll m){
  if (b == 0) return 1;
  if (b == 1) return a;
  if (b&1) return Mult(a, bipow(a, b-1, m), m);
  ll tmp = bipow(a, b/2, m);
  return Mult(tmp, tmp, m);
}

void solve (){
  int N; cin >> N;
  while (N--){
    ll num; cin >> num;
    if (num == 1) cout << "1\n";
    else if (num == 2) cout << "2\n";
    else if (num == 3) cout << "3\n";
    else if (num%3 == 0) cout << bipow(3, num/3, MOD) << '\n';
    else if (num%3 == 1) cout << Mult(4, bipow(3, (num-4)/3, MOD), MOD) << '\n';
    else cout << Mult(2, bipow(3, (num-2)/3, MOD), MOD) << '\n';
  }
}

int main (){
  icisc
  solve();
}

AC CODE

04 矩陣乘法

你還有機會被我的題敘噁心到嗎?我也不知道誒:)

04 矩陣乘法

我相信你們還記得

運算思維在幹嘛對吧

如果真的忘記

我在這邊 speed run 複習

04 矩陣乘法

\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix}
  • 很顯然地,這是一個矩陣
  • 它的行數是 2
  • 它的列數是 3
  • 矩陣 加/減 法在彼此的行列數皆相同時成立
  • 啊就,直接加啊.jpg
\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix} + \begin{bmatrix} 2 & 3 \\ 5 & 7 \\ 11 & 13 \end{bmatrix} = \begin{bmatrix} 3 & 5 \\ 8 & 11 \\ 16 & 19 \end{bmatrix}

04 矩陣乘法

  • 當矩陣 A 的行數等於矩陣 B 的列數時兩者方能相乘
  • a × b 與 b × c 的矩陣相乘後會是 a × c
  • \(c_{ij} = \Sigma^{n}_{k=1}(a_{ik}\,b_{kj})\)
\begin{bmatrix} 1 \;\;& 2 \\ 3 \;\;& 4 \\ 5 \;\;& 6 \end{bmatrix}

\(c_{11} = 1+6+15=22\)

\(c_{12} = 2+8+18=28\)

\(c_{21} = 4+15+30=49\)

\(c_{22} = 8+20+36=64\)

\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}
\begin{bmatrix} 22 & 28 \\ 49 & 64 \end{bmatrix}
  • a 擺在下面,b 擺在上面,搞混會出事

04 矩陣乘法

\(c_{ij} = \Sigma^{n}_{k=1}(a_{ik}\,b_{kj})\)

,把這傢伙用程式實作出來

  • 看起來就很 vector 對吧,你要用 array 我也沒意見啦
vector <vector <ll>> matrix_mult (vector <vector <ll>> a, vector <vector <ll>> b){
  int ax = a[0].size(), ay = a.size(); // x 表行數,y 表列數
  int bx = b[0].size(), by = b.size(); // x 表行數,y 表列數
  if (ax == by) { //a 的行數必須等於 b 的列數
    vector <vector <ll>> ret(ay, vector <ll>(bx, 0));
    for (int i = 0; i < ay; i++){ // 直的跑
      for (int j = 0; j < bx; j++){ // 橫的跑
        for (int k = 0; k < ax; k++){ // 負責計算
          ret[i][j] += a[i][k] * b[k][j];
        }
      }
    }
    return ret;
  }else{
    cout << "Error\n";
    return vector <vector <ll>>();
  }
}

04 矩陣乘法

  • 複雜度...那三層迴圈明顯會導致 \(O(N^3)\)

但其實沒什麼差。實作上這樣就夠了

  • 目前最快的矩陣乘法演算法,也只能達到 \(O(N^{2.3728639})\)

但那個很噁心,然後我也不會

04 矩陣乘法

所以為何要特地教一個純語法的東西?

矩陣快速冪 能拿來優化 DP,使其複雜度多帶一個 log

但那是之後的事了,今天只是前導

你可以先嘗試做做看矩陣快速冪的模板...?

矩陣乘法沒有交換率,但它有 結合率

換言之,它可以砸快速冪進去

04 矩陣乘法

食酢

嘗試按照定義自己做做看吧

它不難,就只是很煩

然後會一直把行與列搞混而已

04 矩陣乘法

矩陣乘法

#include <bits/stdc++.h>
#define icisc ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define ll long long int
#define usll unsigned long long int
#define pii pair <int, int>
#define F first
#define S second
#define pq priority_queue
#define pb push_back
using namespace std;

vector <vector <ll>> matrix_mult (vector <vector <ll>> a, vector <vector <ll>> b){
  int ax = a[0].size(), ay = a.size(); // x 表行數,y 表列數
  int bx = b[0].size(), by = b.size(); // x 表行數,y 表列數
  if (ax == by) { //a 的行數必須等於 b 的列數
    vector <vector <ll>> ret(ay, vector <ll>(bx, 0));
    for (int i = 0; i < ay; i++){ // 直的跑
      for (int j = 0; j < bx; j++){ // 橫的跑
        for (int k = 0; k < ax; k++){ // 負責計算
          ret[i][j] += a[i][k] * b[k][j];
        }
      }
    }
    return ret;
  }else{
    cout << "Error\n";
    return vector <vector <ll>>();
  }
}

void solve (){
  int a, b, c, d;
  while (cin >> a >> b >> c >> d){
    if (b != c){
      cout << "Error\n";
      continue;
    }

    vector <vector <ll>> matrix_a(a, vector <ll>(b, 0));
    vector <vector <ll>> matrix_b(c, vector <ll>(d, 0));
    for (int i = 0; i < a; i++)
      for (int j = 0; j < b; j++)
        cin >> matrix_a[i][j];
    for (int i = 0; i < c; i++)
      for (int j = 0; j < d; j++)
        cin >> matrix_b[i][j];

    vector <vector <ll>> ans = matrix_mult(matrix_a, matrix_b);
    for (int i = 0; i < a; i++) {
      for (int j = 0; j < d; j++) {
        cout << ans[i][j] << ' ';
      }
      cout << '\n';
    }
  }
}

int main (){
  icisc
  solve();
}

AC CODE