# Week 9:單調性與單調佇列 ###### tags: `S3 公開資源` :::info 時間:04/16 9:00 ~ 17:00 地點:成大資工系新館 **65304** 教室 主題:單調性與單調佇列 直播連結:https://youtube.com/live/IJLZFzmlYo8?feature=share ::: # 必作題題解 [TOC] ## 二章後半 ### [CSES 1640 - Sum of Two Values](https://cses.fi/problemset/task/1640/) 撰寫者:Eason 用雙指針去找 複雜度:$O(n)$ :::spoiler code ```cpp= #include<iostream> #include<vector> #include<algorithm> #include<utility> #define ll long long using namespace std; int main(){ ios::sync_with_stdio(0), cin.tie(0); ll n, x; cin >> n >> x; vector <pair<ll, ll> > v; for (int i = 1; i <= n; i++){ ll temp; cin >> temp; v.push_back(make_pair(temp, i)); } sort (v.begin(), v.end()); int l = 0, r = n - 1; while (true){ ll sum = v[l].first + v[r].first; if (l == r){ cout << "IMPOSSIBLE\n"; break; } else if (sum == x){ cout << v[l].second << ' ' << v[r].second << '\n'; break; } else if (sum < x) l++; else if (sum > x) r--; } return 0; } ``` ::: --- ### [UVa 138 - Street Numbers](http://domen111.github.io/UVa-Easy-Viewer/?138) 撰寫者:fishhh 這一題網路上查應該都是數學解。分析一下數學解複雜度,對於每個 n 窮舉($O(n)$),每次驗證是$O(1)$ 所以數學解是$O(n)$ 如果不用數學解呢? 你會發現,在 k 越大的同時,從 $1+...+k$ 的值會大於 $k+...+n$ 在k小於一定值時會小於,在k大於一定值時會大於,這就是他的單調性! 所以我們可以對他二分搜。 所以複雜度會是$O(nlogn)$。 喔不對 這個會TLE QQ 其中我註解掉的的地方是數學解的部分 至於 fd 函數,它的功能是對一個 n 找有沒有可以符合的 k 如果有,就回傳 k 沒有就回傳 0。 :::spoiler 參考程式碼 ```cpp= #include <iostream> #include <cstdio> #include <cmath> using namespace std; long long int fd(int n){ long long int now = 0; long long int add = (1<<20); long long int pre_sum,last_sum; while(add){ if(now+add>n){ add/=2; continue; } pre_sum = ((now+add+1)*(now+add))/2; last_sum = ((n+now+add)*(n-now-add+1))/2; if(last_sum>=pre_sum){ now+=add; } else{ add/=2; } } pre_sum = ((now+add+1)*(now+add))/2; last_sum = ((n+now+add)*(n-now-add+1))/2; if(last_sum==pre_sum){ return now; } return 0; } int main(){ long long int houseNumber = 1; for( int i = 0 ; i < 10 ; ++i ){ bool isAnswer = false; while( !isAnswer ){ ++houseNumber; long long int ret = fd(houseNumber); if(ret){ printf("%10.0d%10.0d\n", ret, houseNumber); isAnswer = 1; } // double lastNumber = (-1 + sqrt(1.0 + houseNumber * houseNumber * 8)) / 2; // if( lastNumber == floor(lastNumber) ){ // printf("%10.0lf%10.0lf\n", houseNumber, lastNumber); // isAnswer = true; // } } } return 0; } ``` ::: 這裡改成 sliding window 的作法。 --- ### [CSES 1660 - Subarray Sums I](https://cses.fi/problemset/task/1660) 撰寫者:Eason 題目要求區間 $[l, r]$ 的和要等於 $x$ 枚舉左界,在左界固定的情況二分搜右界 若要用前綴和求區間 $[l, r]$ 的和 => $prefix[r] - prefix[l - 1]$ :::spoiler code ```cpp= #include<bits/stdc++.h> #define ll long long #define weakson ios::sync_with_stdio(0), cin.tie(0); using namespace std; ll arr[200005], prefix[200005]; int n, x; int bin_search(int lp){ // 左界為 lp // 在利用二分搜求出右界 int l = lp, r = n + 1; // 這邊的 l, r 並不是左、右界 // 而是用來二分搜右界的變數 while (l + 1 <= r){ int mid = (l + r) / 2; ll sum = prefix[mid] - prefix[lp]; if (sum == x){ return 1; } else if (sum > x){ r = mid; } else{ l = mid + 1; } } return 0; } int main(){ weakson; cin >> n >> x; prefix[0] = 0; for (int i = 0; i < n; i++){ cin >> arr[i]; prefix[i + 1] = prefix[i] + arr[i]; } int ans = 0; for (int i = 0; i < n; i++){ // 枚舉左界 ans += bin_search (i); } cout << ans << '\n'; return 0; } ``` ::: --- ### [ZJ g277 - 幸運數字](https://zerojudge.tw/ShowProblem?problemid=g277) 撰寫者:$Jun$-$an$ 這題要在 $[$ $L$ $,$ $R$ $]$ 區間之中找到最小值的位置 $m$ 接著將區間分為 $[$ $L$ $,$ $m$ $-$ $1$ $]$ 跟 $[$ $m$ $+$ $1$ $,$ $R$ $]$ 並分別計算出區間總和 區間總和最大的那個即為幸運數字所在的區間 繼續上述步驟直到 $L$ $==$ $R$。 這裡使用前綴和處理區間總合的問題,總和可能會超過 $int$ 範圍,所以要使用 $long$ $long$ 儲存 區間之中的最小值位置 $m$ 則是使用 $pair$ 將數字跟索引值綁在一起由大到小排序,每次查詢時判斷索引值是否在 $L$ ~ $R$ 之間,如果不是就刪掉,重複直到找到符合的。 需要使用 $I/O$ 優化 :::spoiler 程式碼 ```cpp= #include <iostream> #include <algorithm> #include <utility> #include <vector> #define weak ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0); using namespace std; int nums[300005] = {0}; long long prefix_sum[300005] = {0}; vector<pair<int, int>> save; bool cmp(pair<int, int> l, pair<int, int> r) { return l.first > r.first; } int find_min(int L, int R) { while (true) { pair<int, int> tmp = save.back(); if (L - 1 <= tmp.second && tmp.second <= R - 1) { save.pop_back(); return tmp.second; } else { save.pop_back(); } } } int main() { weak int n; cin >> n; for (int i = 0; i < n; ++i) { cin >> nums[i]; save.push_back(make_pair(nums[i], i)); } sort(save.begin(), save.end(), cmp); for (int i = 1; i <= n; ++i) { prefix_sum[i] = prefix_sum[i - 1] + nums[i - 1]; } int L = 1, R = n; while (L < R) { int mn_idx = find_min(L, R) + 1; if (prefix_sum[mn_idx - 1] - prefix_sum[L - 1] < prefix_sum[R] - prefix_sum[mn_idx]) { L = mn_idx + 1; } else { R = mn_idx - 1; } } cout << nums[R - 1] << '\n'; return 0; } ``` ::: --- ### [ZJ h084 - 牆上海報](https://zerojudge.tw/ShowProblem?problemid=h084) 撰寫者:fishhh 先發現一件事,在 $x$ 由小到大,那麼可以放完這件事成功與否就會類似下面這樣。 x = 1 2 3 4 5 ok= 1 1 1 0 0... 也就是說會有一段 1 再來全部都是 0 所以我們就是要找那個最大的 1 出現的位置。 至於貪心的部分就去看講義有詳細的說明。 :::spoiler ```cpp= #include "iostream" using namespace std; int ary[200010]={},flag[6010]={}; int n,k; bool test(int x){ int cont = 0,i,j; for(i=1,j=1;i<=n&&j<=k;i++){ if(ary[i]>=x)cont++; else{ cont = 0; } if(cont==flag[j]){ cont = 0; j++; } } if(j==k+1){ return 1; } return 0; } int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin>>n>>k; for(int i=1;i<=n;i++)cin>>ary[i]; for(int i=1;i<=k;i++)cin>>flag[i]; int now = 0; int add = (1<<28); while(add){ if(!test(now+add)){ add/=2; } else{ now+=add; } } cout<<now<<"\n"; } ``` ::: --- ### [CSES 1084 - Apartments](https://cses.fi/problemset/task/1084/) 撰寫者:Eason 兩組序列排序後利用雙指針就可以了 (一個指向 applicants 一個指向 apartments) 需要考慮三個情況 1. 公寓剛好符合 2. 公寓太小 3. 公寓太大 - 如果對第 $i$ 人公寓太大,對 $i+1$ 人不一定太大 - 如果對第 $i$ 人公寓太小,下一個公寓可能剛好符合他的需求 複雜度:$O(max(n, m))$ :::spoiler code ```cpp= #include<bits/stdc++.h> #define weakson ios::sync_with_stdio(0), cin.tie(0); using namespace std; int main(){ int n, m, k; cin >> n >> m >> k; int a[200005], b[200005]; for (int i = 0; i < n; i++){ cin >> a[i]; } for (int i = 0; i < m; i++){ cin >> b[i]; } sort (a, a + n); sort (b, b + m); int ap = 0, bp = 0, ans = 0; while (ap < n && bp < m){ if (abs (a[ap] - b[bp]) <= k){ // 剛好符合 ap++; bp++; ans++; } else if (b[bp] > a[ap] + k){ // 公寓太大 ap++; } else if (b[bp] < a[ap] - k){ // 公寓太小 bp++; } } cout << ans << '\n'; return 0; } ``` ::: --- ### [CSES 1641 - Sum of Three Values](https://cses.fi/problemset/task/1641/) 撰寫者:$Jun$-$an$ 給定一個數列,找出三個數字總和為 $target$,回傳這三個數的索引值。 使用 $pair$ 將數字與索引值綁在一起後由小到大排序 接著掃過每個數字,同時用 $two$ $pointer$ 的概念 判斷當前數字 $n$ $+$ $save[L]$ + $save[R]$ 是否等於 $target$ 可以知道 $save$ 是排序過的數列, 如果 $<target$ ,$L+1$ 否則,$R-1$ :::spoiler 程式碼 ```cpp= #include <iostream> #include <algorithm> #include <utility> #include <vector> using namespace std; int nums[5005] = {0}; vector<pair<int, int>> save; int main() { int n, target; cin >> n >> target; for (int i = 0; i < n; ++i) { cin >> nums[i]; save.push_back(make_pair(nums[i], i + 1)); } sort(save.begin(), save.end()); bool flag = false; for (int i = 0; i < n; ++i) { int L = 0, R = n - 1; while (L < R) { if (L == i) { ++L; } else if (R == i) { --R; } else { if (save[i].first + save[L].first + save[R].first < target) { ++L; } else if (save[i].first + save[L].first + save[R].first > target) { --R; } else { cout << save[i].second << ' ' << save[L].second << ' ' << save[R].second << '\n'; flag = true; break; } } } if (flag) break; } if (!flag) cout << "IMPOSSIBLE\n"; } ``` ::: --- ### [ZJ e289 美麗的彩帶](https://zerojudge.tw/ShowProblem?problemid=e289) 撰寫者:fishhh ---