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 你要拿哪一個?
當然是 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 矩陣乘法
- 很顯然地,這是一個矩陣
- 它的行數是 2
- 它的列數是 3
- 矩陣 加/減 法在彼此的行列數皆相同時成立
- 啊就,直接加啊.jpg
04 矩陣乘法
- 當矩陣 A 的行數等於矩陣 B 的列數時兩者方能相乘
- a × b 與 b × c 的矩陣相乘後會是 a × c
- \(c_{ij} = \Sigma^{n}_{k=1}(a_{ik}\,b_{kj})\)
\(c_{11} = 1+6+15=22\)
\(c_{12} = 2+8+18=28\)
\(c_{21} = 4+15+30=49\)
\(c_{22} = 8+20+36=64\)
- 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();
}