# SCIST S4 演算法課程_單調性
:::info
時間:04/28 9:00 ~ 18:00
主題:單調性
直播連結:https://www.youtube.com/watch?v=ZSpqd-9H0g0
:::
## 課程內容
- 單調性
- 講義連結:https://hackmd.io/@sa072686/cp/%2F%40sa072686%2FrJpY0i7PS
# 題解
[TOC]
# 二分搜
### [UVa 10530](http://domen111.github.io/UVa-Easy-Viewer/?10530)
---
### [TOJ 55](https://toj.tfcis.org/oj/pro/55/)
撰寫者:fishhh
待補
:::spoiler Code
```cpp=
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n,ary[1000001],t,q,tot=0;
cin>>n;
for(int i=0;i<n;i++)cin>>ary[i];
sort(ary,ary+n);
cin>>t;
for(int x=0;x<t;x++){
tot=0;
cin>>q;
for(int s=0,e=n-1;e>=s;){
int mid=(s+e)/2;
if(q>ary[mid]){
s=mid+1;
}
else if(q<ary[mid]){
e=mid-1;
}
else{
for(int i=mid;i<n;i++){
if(ary[i]==q){
tot++;
}
else{
break;
}
}
for(int i=mid-1;i>=0;i--){
if(ary[i]==q){
tot++;
}
else{
break;
}
}
break;
}
}
cout<<tot<<"\n";
}
}
```
:::
---
### [ZJ h083](https://zerojudge.tw/ShowProblem?problemid=h083)
---
### [ZJ i401](https://zerojudge.tw/ShowProblem?problemid=i401)
---
### [UVa 10567](http://domen111.github.io/UVa-Easy-Viewer/?10567)
---
### [AtCoder typical90_g](https://hackmd.io/@sa072686/AtCoder_typical90_g)
---
### [TOJ 47](https://toj.tfcis.org/oj/pro/47/)
---
### [Kattis massivecardgame](https://open.kattis.com/problems/massivecardgame)
---
### [Kattis bootstrappingnumber](https://open.kattis.com/problems/bootstrappingnumber)
撰寫者:Jun-an
輸入一個整數 $n$,
找出一個數字 $x$,滿足 $x^x$ = $n$。
這裡利用二分搜在 $1$ ~ $n$ 之間找出 $x$,
浮點數的誤差值在 $1e$-$6$ 以內。
輸出的時候建議使用 `fix << precision(7) << x`
:::spoiler 參考程式碼
```cpp=
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
void Bsearch(double n) {
double l = 0, r = n;
for (int i = 0; i < 101; ++i) {
double mid = l + (r - l) / 2;
if (pow(mid, mid) < n) {
l = mid;
}
else {
r = mid;
}
}
cout << fixed << setprecision(10) << l + (r - l) / 2 << '\n';
}
int main() {
double n;
cin >> n;
Bsearch(n);
}
```
:::
---
### [CSES 1620](https://cses.fi/problemset/task/1620/)
撰寫者:fishhh
假設答案是 $ans_t$,如果給機器超過 $ans_t$ 的時間,就一定可以做完。
反之,如果給所有機器小於 $ans_t$ 的時間,就一定做不完。
所以這就是一個單調性!
我們就對於"時間"去二分搜,每次測試可以單獨寫一個函數來判斷這樣的時間下能做出幾個、能不能做超過 $t$ 個,如果可以就縮右界,如果不可以就縮左界。
```cpp=
#include "iostream"
#include "vector"
#include "cmath"
using namespace std;
#define int unsigned long long
vector<int> v;
int test(int t){
int ret=0;
for(int i:v){
ret+=t/i;
}
return ret;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,t,k;
cin>>n>>t;
for(int i=0;i<n;i++){
cin>>k;
v.push_back(k);
}
int now=0,add=31;
add=(1<<add);
while(add){
while(test(now+add)<t){
now+=add;
}
add/=2;
}
cout<<now+1;
}
```
---
### [ZJ c575](https://zerojudge.tw/ShowProblem?problemid=c575)
撰寫者:Koying
可以發現到,每個基地台的範圍越大,就越有機會成功。
將範圍當作 $x$,成功與否當作 $y$ 畫成圖可發現此函數具有單調性。
因此我們可以對基地台範圍二分搜,找到一個最小且可成功的範圍。
至於如何檢查合法與否?一個半徑為 $r$ 的基地台,其實就是從起點開始延伸 $2r$,所以我們可以利用一個變數紀錄目前這個基地台的範圍,如果服務點超出了範圍,再新架一個新的基地台,並延伸 $2r$
:::spoiler Code
```cpp=
#define MAXN 100005
#define MAXM 1000005
int n, m;
int x[MAXN];
void sol() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> x[i];
}
sort(x, x + n);
int l = 0, r = x[n - 1], mid = (l + r) / 2;
while (l < r) {
mid = (l + r) / 2;
int now = 0, cnt = 0;
for (int i = 0; i < n; i++) {
if (x[i] > now) {
now = x[i] + mid;
cnt++;
}
}
if (cnt > m) {
l = mid + 1;
}
else
r = mid;
}
cout << r << endl;
}
```
:::
---
### [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 1640](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;
}
```
:::
---
### [AtCoder abc212_c](https://atcoder.jp/contests/abc212/tasks/abc212_c)
撰寫者:fishhh
- $O(n^2)$:
兩兩計算絕對值,求最小。
- $O(n\log n)+O(n \log n)$
先將數列A排序(其實換成數列B也行)。
可以想到一件事情,如果要求"與 $b_i$ 絕對值最小"會有兩種狀況
1. 數列 A 裡面小於 $b_i$ 的最大值
2. 數列 A 裡面大於 $b_i$ 的最小值
分別透過一次二分搜就可以完成了
- $O(n\log n) + O(n)$
將數列 A、B 由小到大排序。
邏輯推論一下,可以知道一件事情:與 $b_i$ 絕對值差最小的兩個在 $a_i$ 裡面的數字,必定相鄰。
例如:
$A = \{1,3,5,10\}$
$B_i = 7$ 與 $B_i$ 絕對值差最小的就是 $5,10$ 它們相鄰。
再推論一下:
因為 $B$ 排序過,故 $b_i \leq b_{i+1}$
所以,如果我們知道與 $b_i$ 絕對值差最小的位置。那麼 $b_{i+1}$ 與數列 $A$ 內絕對值差最小的位置一定大於等於與 $b_i$ 絕對值差最小的位置。
所以只要線性掃過就可以了!!!
:::spoiler 參考程式碼
```cpp=
int ary[200010],bry[200010],bbry[200010];
void solve(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>ary[i];
for(int i=1;i<=m;i++)cin>>bry[i];
sort(ary+1,ary+n+1);
sort(bry+1,bry+m+1);
int nm = 1;
bbry[1] = bry[1];
for(int i=2;i<=m;i++){ //去重(bry)
if(bry[i]!=bry[nm]){
bry[++nm] = bry[i];
}
}
m = nm;
int ans = 1e9;
int pt = 1;
for(int i=1;i<=n;i++){
while(pt<m&&bry[pt+1]<=ary[i]){ //如果有等於 那其實不管選左邊還是右邊最後答案都是 0
pt++;
}
ans = min(ans,abs(ary[i]-bry[pt]));
if(pt!=m) ans = min(ans,abs(ary[i]-bry[pt+1]));
}
cout<<ans<<"\n";
}
```
:::
---
### [Kattis downtime](https://open.kattis.com/problems/downtime)
撰寫者:Eason
---
### [CSES 1660](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;
}
```
:::
---
### [AtCoder abc038_c](https://atcoder.jp/contests/abc038/tasks/abc038_c)
撰寫者:fishhh
只需要算有幾個連續的最長單調區間就好。例如數列為 $\{1,2,5,3,5,3,2,3\}$
就可以分成是:$\{1,2,5\} , \{3,5\} , \{3\},\{2,3\}$ 這四組連續的單調嚴格遞增區間。
實作的部分:假設 $ary_i \sim ary_j$ 有單調性,如果 $ary_{j+1}>ary_j$ 那麼這個單調區間就可以包含 $ary_{j+1}$。否則就是創造了一個新的單調區間出來。
:::spoiler
```cpp=
int ary[100010];
void solve(){
int n;
long long ans = 0,acc = 0;
cin>>n;
for(int i=1;i<=n;i++)cin>>ary[i];
for(int i=1;i<=n;i++){
acc++;
if(i==n||ary[i+1]<=ary[i]){
ans += acc*(acc+1)/2;
acc = 0;
}
}
cout<<ans<<"\n";
}
```
:::
---
### [AtCoder abc210_c](https://atcoder.jp/contests/abc210/tasks/abc210_c)
---
### [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;
}
```
:::
---
### [AtCoder abc224_e](https://atcoder.jp/contests/abc224/tasks/abc224_e)
---
### [CSES 1645](https://cses.fi/problemset/task/1645/)
---
### [TIOJ 1370](https://tioj.ck.tp.edu.tw/problems/1370)
---
### [ZJ f174](https://zerojudge.tw/ShowProblem?problemid=f174)
---
### [CSES 1619](https://cses.fi/problemset/task/1619/)
---
### [AtCoder typical90_bl](https://atcoder.jp/contests/typical90/tasks/typical90_bl)
---
### [ZJ g597](https://zerojudge.tw/ShowProblem?problemid=g597)
---
### [AtCoder abc216_e](https://atcoder.jp/contests/abc216/tasks/abc216_e)
---
### [AtCoder typical90_ab](https://hackmd.io/@sa072686/AtCoder_typical90_ab)