# 枚舉2 by:hush --- ## 枚舉子集(subset) ---- - 什麼是子集? 從一個母集合裡挑若干個元素,可以全挑也可以不挑 (集合沒有順序也不重複) ---- - 舉例: 定義一個集合$S=\{1,3,4,6\}$ 則 $\{4,1\}或\{1,3,4,6\}或\{ \}$ 為$S$的子集 而 $\{1,2\}或\{1,3,4,6,8\}$ 不是 ---- - 實作方法: 1. 遞迴枚舉1 2. 遞迴枚舉2 3. 位元枚舉(迴圈版) 時間複雜度都是$O(2^n)$ ---- - 遞迴枚舉1: 若共有$n$個元素,當下選到第$k$個,遞迴枚舉下一個元素為第$k+1$ ~ $n$的情況,從$0$(代表空的元素)開始遞迴 ---- 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(25); vector<bool> in(25,0); int n; void subset(int k) { if (k==n+1) //枚舉結束 { for (int i=1;i<=n;i++) if (in[i]) cout << a[i] <<' '; cout << '\n'; return; } in[k]=1; for (int i=k+1;i<=n+1;i++) subset(i); in[k]=0; } signed main() { //colinorz; cin >> n; for (int i=1;i<=n;i++) cin >> a[i]; subset(0); } ``` ---- - 遞迴枚舉2: 若共有$n$個元素,當下已決定前$k-1$個元素是否在子集合內,則分別枚舉第$k$個元素存在以及不存在的情況(我通常使用0-base寫法) ---- code: ```cpp=10 //前面一樣 void subset(int k) { if (k==n) //枚舉結束 { for (int i=0;i<n;i++) if (in[i]) cout << a[i] <<' '; cout << '\n'; return; } in[k]=1; //第k個元素存在 subset(i); in[k]=0; //記得設回不存在 subset(i); } signed main() { //colinorz; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; subset(0); } ``` ---- - 迴圈枚舉: 要用到一些位元運算的東西([點這裡自學](https://apcs.guide/courses/beginner/tricks/binary-operator)) 概念是把子集合用一個$x$整數來表示$(1\le x\lt 2^{|S|})$,$x$二進位表示中的第$k$位若為1,則第$k$個元素在子集合內 - 有$3$個元素的情況: |$x$| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | - | - | - | - | - | - | - | - | - | |$2^0$| 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | |$2^1$| 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | |$2^2$| 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | ---- code: ```cpp=10 //前面一樣 void subset() { for (int i=0;i<(1<<n);i++) //「a<<b」代表a*2^b { for (int j=0;j<n;j++) if (i&(1<<j)) //第j個元素是否存在子集 { cout << a[j] << ' '; } cout << endl; } } signed main() { //colinorz; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; subset(); } ``` ---- - 練習題: [CSES1623](https://cses.fi/problemset/task/1623) ---- - CSES1623題解: 枚舉所有子集,求子集合加總和補集合加總的差,取最小的差輸出 - 小技巧: 這題我會用遞迴枚舉而不是位元枚舉(迴圈),原因是遞迴枚舉邊枚舉就可以邊算加總,而位元枚舉每個子集都要重算一次,速度較慢 ---- 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; int n,sum=0,cnt=0,ans=1e18; vector<int> a(25); void mind(int i) { if (i==n) { ans=min(ans,abs(cnt-(sum-cnt))); return; } cnt+=a[i]; //枚舉在目前情況有包含a[i]的子集合 mind(i+1); cnt-=a[i]; //枚舉不包含a[i]的 mind(i+1); } signed main() { //colinorz; cin >> n; for (int i=0;i<n;i++) cin >> a[i],sum+=a[i]; mind(0); cout << ans <<'\n'; } ``` --- ### 折半枚舉 ---- $2^{20}\approx 1.04\times 10^6$ ||( nice )|| $2^{40}\approx 1.09\times 10^{12}$ ||( = = )|| ---- 直接看題目([zj_a161](https://judge.cchs.chc.edu.tw/ShowProblem?problemid=a161)): - 輸入$n$個正整數$A_1$~$A_n$,和一個整數$P$ - 請計算$A$的各種子集合中,其加總值最接近$P$但不超過$P$的值是多少。 - $A_i, P\le 2^{60}$,$0 < n ≤ 38$ ---- - $n=38$直接枚舉,$O(2^n)$肯定會TLE,所以: 1. 把$n$平分成兩堆,分別枚舉子集 $O(2^{n/2})$ 2. 挑其中一堆進行排序 $O(2^{n/2}\cdot log(2^{n/2}))$ 3. 遍歷另一堆,對已排序那堆二分搜 $O(2^{n/2}\cdot log(2^{n/2}))$ - 註:二分搜也可用雙指標代替,重點是要讓原本的$O(2^{\tfrac{n}{2}^2})$變更快 ---- AC code: ```cpp= #include<bits/stdc++.h> #define int long long #define endl '\n' #define colinroz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; vector<int> a(45); vector<vector<int>> subset(5); void enu(int cur,int l,int r,int sum=0) //[l,r) { if (l==r) { subset[cur].emplace_back(sum); return; } enu(cur,l+1,r,sum+a[l]); //有a[l] enu(cur,l+1,r,sum); //沒有 } signed main() { //colinorz; int n,p,ans=-1e18; cin >> n >> p; for (int i=0;i<n;i++) cin >> a[i]; int mid=n/2+n%2; enu(0,0,mid); enu(1,mid,n); sort(subset[1].begin(),subset[1].end()); for (int& i : subset[0]) { auto it=upper_bound(subset[1].begin(),subset[1].end(),p-i); if (it==subset[1].cbegin()) continue; //.cbegin()代表常數iter ans=max(ans,i+*(it-1)); } cout << ans << endl; } ``` --- ## 枚舉排列 ---- - 什麼是排列? 就是一個序列的各種順序(註:交換兩個值相同的元素還是同一種排列) - 舉例: 序列$[4, 1, 2]$的所有排列有$[1, 2, 4]$, $[1, 4, 2]$, $[2, 1, 4]$, $[2, 4, 1]$, $[4, 1, 2]$, $[4, 2, 1]$ ---- 實作方法: 1. 手刻遞迴 2. STL函式 ---- - 手刻函式: 改一下前面子集的**遞迴枚舉1**就可以了。 時間複雜度$O(n\times n!)$有點高,所以不常用。但有時候題目會逼你寫,所以建議還是要知道怎麼寫 ---- 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(25),now; vector<bool> used(25,0); int n; void permutation(int k) { if (k==n) { for (int i=0;i<n;i++) cout << now[i] << " \n"[i==n-1]; return ; } for (int i=0;i<n;i++) { if(used[i]) continue ; //用過了就不能再用 now.push_back(a[i]); used[i]=1; permutation(k+1); now.pop_back(); used[i]=0; //記得改回來! } } signed main() { //colinorz; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; permutation(0); } ``` ---- - STL函式: **先排序**,然後用std::next_permutation函式。 時間複雜度$O(n!)$,不會把相同的值重複排序 ---- code: ```cpp=12 //前面一樣 signed main() { //colinorz; cin >> n; for (int i=0;i<n;i++) cin >> a[i]; sort(a.begin(),a.begin()+n); //要先排序!!! do { for (int i=0;i<n;i++) cout << a[i] << " \n"[i==n-1]; }while (next_permutation(a.begin(),a.begin()+n)); //行尾要分號!!! } ``` ---- - 練習題: [cses1622](https://cses.fi/problemset/submit/1622/) ---- - cses1622 題解: ||首先理解題目意思,然後直接實作即可|| ---- 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; string s; signed main() { //colinorz; cin >> s; sort(s.begin(),s.end()); int cnt=0; do { cnt++; } while (next_permutation(s.begin(),s.end())); cout << cnt << endl; //枚舉完會變回原本順序 do { cout << s << endl; } while (next_permutation(s.begin(),s.end())); } ``` --- # 謝謝大家
{"description":"type: slide","title":"枚舉2","contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":6780,\"del\":356}]"}
    114 views