# 枚舉1 by:hush --- ## 枚舉? ---- 很直觀的做法 就是把所有的可能性都檢查一遍,確保萬無一失 --- ## 一般的枚舉 ---- 舉例:檢查一個數$n$($2\le n\le 10^6$)是否為質數 ---- 枚舉$[2,n-1]$ * 若存在任意一個數可以整除$n$ 則$n$不為質數 ---- ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; signed main() { //colinorz; int n; cin >> n; bool ans=1; for (int i=2;i<n;i++) if (n%i==0) ans=0; cout << ans << endl; } ``` --- ### 剪枝 ---- 剛剛那個問題有一個明顯的性質: * 若存在兩個自然數$p,q$為$n$的因數($n=p\cdot q$) 則$min(p,q)\le \sqrt n$ (否則$p\cdot q\gt n$) * 所以其實只需要枚舉$[2,\lfloor \sqrt n\rfloor ]$ ---- 把 "$i\lt n$" 改成 "$i\times i\le n$" 即可 ```cpp=14 for (int i=2;i*i<=n;i++) if (n%i==0) ans=0; ``` --- 再看一題: 給你一個字串$s$,求$s$內的最長回文子字串的長度 $(\vert s\vert\le 10^4)$ ---- 子字串代表從原字串取一段**連續**的子字串 例如"csdcorz"是母字串,那麼: * "csdc" 是子字串 * "csorz" 不是 * "csdcorz" 是 * "" 是 ---- * 直覺的想法: 枚舉子字串的左界與右界,並檢測枚舉的子串是否為回文,紀錄最大的區間大小。 ---- 範例程式碼: ```cpp= #include <bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; string s; signed main() { //colinorz; int ans=0; cin >> s; for (int i=0;i<s.size();i++) for (int j=i;j<s.size();j++) { if (j-i+1<=ans) continue; //剪枝 bool isp=1; for (int l=i,r=j; isp&&l<r; l++,r--) if (s[l]!=s[r]) isp=0; if (isp) ans=j-i+1; } cout << ans <<'\n'; } ``` ---- * 複雜度分析: 枚舉左右界的時間複雜度$O(\vert s\vert ^2)$,檢查是否符合條件$O(\vert s\vert)$,相乘$O(\vert s\vert^3)$,題目的$\vert s\vert \le 10^4$,帶進去要$10^{12}$次,TLE。 ---- 仔細分析回文的性質會發現: * 若$s[l]$~$s[r]$不是回文 則$s[l-1]$~$s[r+1]$也絕對不是 * 若$s[l]$~$s[r]$是回文 則只需判斷$s[l-1]=s[r+1]$成立與否即可 共通點是他們的字串中心一樣 ---- 前面的做法會重複檢查到很多字串中心一樣的字串,所以我們改成枚舉每個字串中心,看可以擴展到多大。 ---- 範例程式碼: ```cpp= #include <bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; string s; signed main() { //colinorz; int ans=0; cin >> s; for (int i=0;i<s.size();i++) { for (int l=i,r=i;l>=0&&r<s.size()&&s[l]==s[r];l--,r++) ans=max(ans,r-l+1); for (int l=i,r=i+1;l>=0&&r<s.size()&&s[l]==s[r];l--,r++) ans=max(ans,r-l+1); } cout << ans <<'\n'; } ``` ---- * 複雜度分析: 中心點數量$O(\vert s\vert)$,擴展次數不超過$O(\vert s\vert)$,所以相乘不超過$O(\vert s\vert^2)$,不會TLE。 --- ### 練習題 ---- - [csdc358](https://csdc.tw/problem/358/) - [csdc396](https://csdc.tw/problem/396/) ---- - CSDC 358題解 枚舉第一個骰子的每一面,對於它的第$i$面枚舉第二個骰子的每一面,檢查兩個相加是否符合特定數 時間複雜度:$O(面數^2)$ - 另解: 排序第二個骰子,枚舉第一個骰子,對於它的第$i$面二分枚舉第二個骰子 時間複雜度:$O(面數 log 面數)$ ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; vector<int> a(1005),b(1005); signed main() { int n,m,q,c=0; cin >> n >> m; for (int i=0;i<n;i++) cin >> a[i]; for (int i=0;i<m;i++) cin >> b[i]; cin >> q; for (int i=0;i<n;i++) for (int j=0;j<m;j++) if (a[i]+b[j]==q) c++; if (c==0) cout << 0 <<'\n'; else if (c==n*m) cout << n*m << '\n'; else cout << c/__gcd(c,n*m) << '/' << n*m/__gcd(c,n*m); //不使用gcd函數也可使用枚舉 } ``` ---- - CSDC 396題解 枚舉$1$~$n$天,對於第$i$天枚舉每一種報告,判斷當天是否有超過三種報告(也就是$i$是否在至少三個區間內) - 另解 使用差分序列(有興趣的可以[自學](https://apcs.guide/courses/advanced/tricks/difference)或是以後可能會教) ---- ```cpp= #include<bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define pii pair<int,int> #define fi first #define se second using namespace std; vector<pii> r(10005); signed main() { //colinorz; int n,m,ans=0; cin >> n >> m; for (int i=0;i<m;i++) { int a,b; cin >> a >> b; r[i]={a,b}; } for (int i=1;!ans && i<=n;i++) { int cnt=0; for (int j=0;cnt<3 && j<m;j++) if (r[j].fi<=i&&i<=r[j].se) cnt++; if (cnt>=3) ans=1; } if (ans) cout << "No\n"; else cout << "Yes\n"; } ``` --- ## 進階的枚舉 --- ### 二分枚舉 ---- 又稱為二分搜 - 概念:就是透過枚舉中點每次排除一半可能性的枚舉 - 可用在有圖形具有單調性(遞增/遞減)的問題 題目關鍵字:找出符合條件的值最大(小)可以到多大(小) ---- #### 丟雞蛋問題 ![drop_egg2](https://hackmd.io/_uploads/H17e9dITC.jpg) *by : chatgpt* ---- 在一個$n$層的大樓,給你一種超硬的雞蛋無限多顆 測試雞蛋最低在哪一層會開始破掉$(n\le 10^{18})$ ---- 如果從第一層開始去一層一層慢慢砸效率太低 最多會算到$n$次,複雜度太高 ---- 在經過精密的計算後發現雞蛋破的情況長這樣: 破了 破了 ⁝ 破了 破了 沒破 ⁝ 沒破 沒破 ---- 所以當你在第$k$層丟一顆雞蛋 * 若它沒破,則$1$ ~ $k$層都沒破 否則$k$ ~ $n$層都是破了 ---- 那每次都從一半檢查,就可以排除一半可能性 直到找到沒破和破的交界處,複雜度$O(log$ $n)$ --- - 應用:終極密碼 在一個遞增(或減)的序列找到第一個大於(等於)或小於(等於)某數$k$的位置$i$ ---- - 實作方法(以遞增、大於等於舉例): 1. 手刻(常用於枚舉數值) 2. 使用STL(常用於搜尋) ---- #### 手刻: ---- 變數 - $l$ 代表枚舉範圍的左界 - $r$ 代表枚舉範圍的右界 - $mid$ 代表枚舉的點 ---- - $mid=(l+r)/2$ <span>$l+r太大可能會overflow$<!-- .element: class="fragment" data-fragment-index="1" --></span> - $mid=l+(r-l)/2$ <span>用減法所以不會<!-- .element: class="fragment" data-fragment-index="2" --></span> ---- ![bs_meme](https://hackmd.io/_uploads/ry5ShZpaC.png) ---- 找到第一個$\ge k$的位置 ```cpp= int b_search(int l,int r,int k) { while(l<r) //[l,r) { int mid=l+(r-l)/2; if (a[mid]>=k) //符合條件 r=mid; //把r移到已知符合條件的最小位置 else //不符合 l=mid+1; //把l移到不符合的下一個位置 } return r; } ``` ---- 使用STL: - lower_bound(a,a+n,k)找到第一個$\ge k$的位置 - upper_bound(a,a+n,k)找到第一個$\gt k$的位置 ```cpp= #include<bits/stdc++.h> using namespace std; constexpr int n=6; int a[n]={1,3,4,6,8,9}; signed main() { cout << lower_bound(a,a+n,0)-a <<'\n'; //0 cout << lower_bound(a,a+n,4)-a <<'\n'; //2 cout << upper_bound(a,a+n,4)-a <<'\n'; //3 cout << lower_bound(a,a+n,11)-a <<'\n'; //6(=n) } ``` ---- 例題:[csdc199](https://csdc.tw/problem/199/) 因為有單調性,可使用二分搜 時間複雜度$O(Q\cdot log$ $n)$ ---- 手刻函式解: ```cpp= #include<bits/stdc++.h> using namespace std; int a[100005]; int b_search(int l,int r,int k) { while(l<r) //[l,r) { int mid=l+(r-l)/2; if (a[mid]>=k) r=mid; //把r移到符合條件的位置 else l=mid+1; //把l移到不符合的下一個位置 } return r; } signed main() { int n; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; int q; cin >> q; while (q--) { int x; cin >> x; cout << b_search(0,n,x) << '\n'; } } ``` ---- 用STL解: ```cpp= #include<bits/stdc++.h> using namespace std; int a[100005]; signed main() { int n; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; int q; cin >> q; while (q--) { int x; cin >> x; cout << lower_bound(a,a+n,x)-a << '\n'; } } ``` --- ### 練習題 ---- - [csdc65](https://csdc.tw/problem/65/) - [csdc236](https://csdc.tw/problem/236/) - [APCS 202201 P4](https://zerojudge.tw/ShowProblem?problemid=h084) ---- - CSDC 65題解 題意:在遞增序列中找到最後一個元素大小$\le X$的位置,也就是第一個$\gt X$的前一個 可以手刻二分搜或使用upper_bound ---- AC code 1: ```cpp= #include<bits/stdc++.h> using namespace std; int a[100005]; int b_search(int l,int r,int k) { //找到第一個大於k的位置-1 while (l<r) { int mid=l+(r-l)/2; if (a[mid]>k) //符合條件 r=mid; else l=mid+1; } return r-1; } signed main() { int n,m; cin >> n >> m; for (int i=1;i<=n;i++) cin >> a[i]; while (m--) { int x; cin >> x; cout << b_search(1,n+1,x) << '\n'; //因為陣列範圍是[1,n]所以要再加1 } } ``` ---- AC code 2: ```cpp= #include<bits/stdc++.h> using namespace std; int a[100005]; signed main() { int n,m; cin >> n >> m; for (int i=1;i<=n;i++) cin >> a[i]; while (m--) { int x; cin >> x; cout << (upper_bound(a,a+n+1,x)-a)-1 << '\n'; } } ``` ---- CSDC 236題解 1. 若容積為$v$可以收集所有的雨水,則容積$\gt v$也一定可以,也就是定義函數$k=f(v)$為「對於容積$v$可收集的(雨水)杯數為$k$」,則$f(v)$具有單調性,所以可以對$v$做二分搜。 2. 二分搜的上下界( $r$ $\&$ $l$ ): - $v$至少要跟最大杯的水一樣大,否則一定無法把水倒完,$l=min(w_i)$ - 若$v=$全部水的總體積,則一定可以倒完所有水,再大就浪費了,$r=\sum{w_i}$ ---- 3. 實作出$f(v)$ - 若$f(v)=m$,去使用$\gt r$的$v$都會浪費空間,則$r=v$ - 否則罐子太小,使用$\le l$的$v$都不可能成立,則$l=v+1$ 4. 做到$l\not\lt r,$由於$r$一直是最小可能的答案,因此答案最後會是$r,(其實l=r)$ - 複雜度分析:二分搜複雜度$O(log(r-l))$,也就是$O(log(n\cdot w_i))$,每次檢查要$O(n)$,所以總複雜度是$O(n\cdot log(n\cdot w_i))$ ---- AC code: ```cpp= #include <bits/stdc++.h> #define int long long #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); #define endl '\n' using namespace std; int n,m,w[5005]; int f(int v) { int cnt=1,rem=v; for (int i=1;i<=m;i++) //第幾罐 { while (cnt<=n && rem>=w[cnt]) rem-=w[cnt++]; if (cnt>n) return cnt; //裝完了!!! rem=v; } return cnt; } signed main() { //colinorz; int t; cin >> t; int wmax=0,sum=0; while (t--) { cin >> m >> n; for (int i=1;i<=n;i++) { cin >> w[i]; sum+=w[i]; wmax=max(wmax,w[i]); } int l=wmax,r=sum; while (l<r) { int mid=l+(r-l)/2; if (f(mid)>n) r=mid; //因為我偷懶讓他多加一 else l=mid+1; } cout << r << endl; wmax=sum=0; } } ``` ---- - APCS 202201 P4 題解 ---- AC code ```cpp= ``` --- ### 雙指標 ---- - **雙指標**(Two Pointers)又稱**滑動視窗**(Sliding Window),是一種神奇的解題小技巧,用來(快速的)在一個序列中枚舉兩個指標。 - 常見問題:求序列中兩數相加(乘)等於x的個數、長度為$s$的子序列和極值、子序列和大(小)於$k$的最長長度... ---- - 直接看題目: 給你一個正整數$n\le 10^6$,輸出所有和為$n$的連續正整數數列(至少兩個數) 範例輸入: ```cpp= 9 ``` 範例輸出: ```cpp= 2 3 4 4 5 ``` ---- - 題解: 使用雙指針$l, r$代表一個正整數序列$l$~$r$(包含) - 分三種情況: 若序列和$= n$,則輸出$l$~$r$ 若序列和$\le n$,則$r$加一 若序列和$\gt n$,則$l$加一 - 時間複雜度: $l, r$ 最多只會從一跑到$1$~$n$,總共跑$2n$次,所以時間複雜度為$O(n)$ ---- AC code: ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; signed main() { //colinorz; int n; cin >> n; int l=1,r=1,sum=1; //[l,r] while (r<n) { if (sum==n) { for (int i=l;i<=r;i++) cout << i << " \n"[i==r]; } if (sum<=n) r++,sum+=r; else sum-=l,l++; } } ``` ---- 進階類題:[zerojudge_k464](https://zerojudge.tw/ShowProblem?problemid=k464) ---- 題解: - 變數: 1. 用雙指標$l, r$代表一個區間$p[l]$~$p[r]$(包含) 2. 用map紀錄區間內每個地主有幾塊地 3. 用一個變數紀錄區間內地主數量 - 執行(過程中要維護map): 1. 每次先讓$r$加一 2. 把$l$加一,重複直到區間內地主數量$\le k$ 3. 更新答案 ---- AC code: ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; vector<int> a(200005); map<int,int> cnt; signed main() { //colinorz; int n,k,ams; cin >> n >> k; for (int i=1;i<=n;i++) cin >> a[i]; int l=1,r=1,ans=0,sum=0; //[l,r] for (;r<=n;r++) { cnt[a[r]]++; if (cnt[a[r]]==1) sum++; for (;sum>k;l++) { cnt[a[l]]--; if (cnt[a[l]]==0) sum--; } ans=max(ans,r-l+1); } cout << ans << endl; } ``` --- # 謝謝大家
{"title":"枚舉1","description":"type : slide","contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":11457,\"del\":496}]"}
    153 views