<style> .reveal .slides { text-align: left; font-size:30px; } </style> ## Struct and Recursive ---- * 結構體 (struct) * 遞迴 (全域變數) ---- ## Struct * 性質 * 用法 * 實作 ---- ## 性質 大家應該或多或少有聽說過物件導向,那所謂物件導向是class裡面會有私有成員(private) : 不被其他函式所使用 以及 公開成員(public) : 可被其他函數所調用 struct跟class常常被拿來做比較,跟class的差別就是裡面的元素都是public的,也不需要給他額外的宣告,只需要在裡面宣告型態類別就好 struct就可以宣告結構,結構就是可以自訂不同的資料型態綑在一起,他們就會是一個共同的整體,裡面的變數是塞什麼都可以,像是上禮拜學到的STL那些容器等,也都可以放進去 ---- ## 用法 具體的用法有兩種,可以把它整體看成一個大的自訂型態,然後分別有兩種宣告方式。 ```cpp= struct node{ int a,b,c; }v[MXN]; ``` ```cpp= struct node{ int a,b,c; }; int main(){ node v; } ``` ---- struct有非常多常用的用法,因為它可以把大部分的東西都包在一起,尤其是之後學習到資料結構時,將大部分的函式也包進去是一個很常見的用法,就相當於你自訂了一種新的資料型態叫做node。 example : BIT(樹狀數組) , MaxClique , Flow ... 而且它也能大幅縮短很多問題的實作碼量。 ---- ## 實作 假設我們有很多筆資料,可能是學號,姓名等,然後我們需要對它排序 ```txt 5 4 LeeShoW 5 Jakao 2 leeshow 3 LeeShow 1 Leeshow ``` 那我們要怎麼快速的對其進行排序呢? ---- 也就是說 當你同一筆資料有許多維度的時候,你就會需要使用struct把他們綑成一團,使得程式實作方便,另外排序當然也可以自己宣告,以下是自己宣告的做法 : ```cpp= struct node{ int a,b,c; //對它的1 2 3維度去排序 }v[MXN]; bool cmp(node x,node y){ if(x.a!=y.a) return x.a<y.a; else if(x.b!=y.b) return x.b<y.b; else return x.c<y.c; } int main(){ cin>>n; for(int i=0;i<n;i++) cin>>v[i].a>>v[i].b>>v[i].c; sort(v,v+n,cmp); } ``` ---- 那如果是在裡面放函式呢? ```cpp= struct student{ int id; string name; void new_student(int now,string str){ for(int i = 0; i<str.size(); i++){ str[i] = toupper(str[i]); } cout<<now<<" "<<str<<"\n"; } }Function; int main(){ string str; int cnt; cin>>cnt>>str; Function.new_student(cnt,str); } ``` 就要在呼叫它的時候增加call的名稱 ---- 那當然也可以用pair去處理 ```cpp= struct student{ pair<int,string> p; void new_student(pair<int,string> p){ for(int i = 0; i<p.second.size(); i++){ p.second[i] = toupper(p.second[i]); } cout<<p.first<<" "<<p.second<<"\n"; } }Class; int main(){ string str; int cnt; cin>>cnt>>str; Class.new_student({cnt,str}); } ``` 所以當然裡面想要放什麼都可以。我們也可以在裡面加入各式各樣的函式,端看你想要return什麼,然後再去作出許多方便的操作,以後上到模板的時候就會有很多example,當然網路上也有許多參考資料。 --- ## 全域變數,遞迴 ---- ## 全域變數 ---- 全域變數就是指在每一個函數中都可以使用的變數,且值是互通的,也不需要在額外宣告一次。而與全域變數相對的就是區域變數,它只能夠在每個宣告過它的函式中進行使用。 ---- 那一開始先實作一些簡單的例子 ```cpp int MAX(int x,int y){ if(x>y) return x; else return y; } int main(){ int a,b; cin>>a>>b; cout<<MAX(a,b); } ``` 這個應該是大家會的東西 ---- 那如果函式沒有要傳遞東西的話,當然也可以這樣寫 ```cpp int a,b; int MAX(){ if(a>b) return a; else return b; } int main(){ cin>>a>>b; cout<<MAX(); } ``` 這樣就是使得 $a$ 和 $b$ 變成每個函式都可以使用的變數 ---- 那在APCS觀念題裡面,有許許多多類似這樣的題目,詢問你輸出是多少,由此可知,不要重複命名也是很重要的 point ! ```cpp= int a = 6; void print(){ cout << a << endl; } int main(){ int a = 3; cout << a << endl; print(); } ``` ---- 其中全域變數的順序很重要,因為考慮到它是個別獨立的區塊,所以你當前函式需要用到的其他函式,需要放在當前函式的前面,以下為例子。 ```cpp= bool cmp(node x,node y){ return x.a<y.a; } struct node{ int a,b,c; }v[MXN]; int main(){ cin>>n; for(int i=0;i<n;i++) cin>>v[i].a>>v[i].b>>v[i].c; sort(v,v+n,cmp); } ``` ---- 若把陣列開在全域變數,相比把陣列開在區域變數裡,可以開的更大,所以好習慣就是把陣列開在全域。 因為他還會自動幫你清零,若開在區域他的初始值會亂跳,因為開在區域變數會變成動態宣告陣列。 ---- ## 枚舉 ---- ## 窮舉排列(不會出現重複數字) ---- 如果我們今天要求出 1-2-3-4-5 的全排列呢(按照字典序)? - next_permutation - 自己實作 那因為今天是窮舉的課程,而且也不是每種情況都可以用next_permutation,所以我們就實作看看 ---- 首先我們會需要儲存進陣列 (array) 然後排序,這樣不管數字是多少都可以排序它。 然後要生成一個用來記錄答案的陣列 (ans) 接下來就要記錄每一個位置是否出現過這個數字 (vis),而當目前衍生的遍歷完後,要記得回朔。 然後理所當然的要從第一項開始遍歷 ---- 首先是讀入以及排序,然後呼叫遞迴函數 ```cpp= int arr[N]={}; int ans[N]={}; bool vis[N]={}; int n; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); //排序 dfs(1); //呼叫遞迴函數 } ``` ---- 第二步呼叫函數後,會需要做的事情是檢查每一項是否有跑過,沒跑過的話就增加到當前傳遞的ans數組。 ```cpp= for(int i=1;i<=n;i++){ if(!vis[i]){ // 檢查當前這項是否在ans裡 ans[index]=arr[i]; // 將當前這項加入ans裡 vis[i]=1; // 標記為有在裡面 dfs(index+1); // 呼叫下一個遞迴 ans[index]=0; // 把答案移除 vis[i]=0; // 這個遞迴結束後移除標記 } } ``` ---- 那千萬不能忘記我們需要設下終止條件,那就是當目前的index已經超過n的範圍的時候,我們就不需要再繼續遍歷了,並且輸出答案 ```cpp= if(index>n){ // 超過陣列範圍 for(int i=1;i<=n;i++){ // 輸出答案 cout<<ans[i]<<" \n"[i==n]; } } ``` ---- 若是對遞迴的過程不了解,可以在每次遞迴時嘗試輸出index和vis數組了解它到底發生甚麼事情 ```cpp= cout<<index<<"\n"; for(int i=1;i<=n;i++){ cout<<vis[i]<<" \n"[i==n]; } ``` ---- 會發現它其實就是對vis的每一項去作0/1的組合,只是順序不一樣 ![](https://hackmd.io/_uploads/H1WmYDNa3.png) *vis的輸出狀態 ---- ```cpp #include<algorithm> #include<iostream> using namespace std; const int N=1e5+5; int arr[N]={}; int ans[N]={}; bool vis[N]={}; int n; void dfs(int index){ if(index>n){ // 超過陣列範圍 for(int i=1;i<=n;i++){ // 輸出答案 cout<<ans[i]<<" \n"[i==n]; } } for(int i=1;i<=n;i++){ if(!vis[i]){ // 檢查當前這項是否在ans裡 ans[index]=arr[i]; // 將當前這項加入ans裡 vis[i]=1; // 標記為有在裡面 dfs(index+1); // 呼叫下一個遞迴 ans[index]=0; // 把答案移除 vis[i]=0; // 這個遞迴結束後移除標記 } } } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); //排序 dfs(1); //呼叫遞迴函數 } ``` ---- ## - 窮舉排列(會出現重複數字) ---- 如果會出現重複數字,那剛剛那種枚舉方式就會錯,因為我們會需要避免index一樣但是數字重複的情況,我們需要避免在同一個位置填入相同的數字。 ---- 所以我們要做的其實就是紀錄上一個出現的數字,避免在最新遞迴到的那一個重複出現就好 ! ```cpp= void dfs(int index){ int pre=0; // 每次都要初始化最後出現的數字 } ``` ---- 在判斷的地方加上當前這個數字是否與前一個相同 ```cpp= for(int i=1;i<=n;i++){ if(!vis[i]&&arr[i]!=pre){ // 沒出現過,且與前一個不相同 pre=arr[i]; // 因為採用了,所以取代掉上一個數字 vis[i]=1; ans[index]=arr[i]; dfs(index+1); vis[i]=0; ans[index]=0; } } ``` ---- ```cpp #include<algorithm> #include<iostream> using namespace std; const int N=1e5+5; int arr[N]={}; int ans[N]={}; bool vis[N]={}; int n; void dfs(int index){ int pre=0; if(index>n){ // 超過陣列範圍 for(int i=1;i<=n;i++){ // 輸出答案 cout<<ans[i]<<" \n"[i==n]; } } for(int i=1;i<=n;i++){ if(!vis[i]&&arr[i]!=pre){ // 沒出現過,且與前一個不相同 pre=arr[i]; // 因為採用了,所以取代掉上一個數字 vis[i]=1; ans[index]=arr[i]; dfs(index+1); vis[i]=0; ans[index]=0; } } } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); //排序 dfs(1); //呼叫遞迴函數 } ``` ---- ## 枚舉子集 ---- 對於一個集合,枚舉出它所有的子集(subset),那這就其實也不難,因為對於每一個vis來說,就只有取和不取兩種方式。 自然而然也是一樣的起手式,就只是少了一個ans紀錄答案,因為可以直接依照vis的布林值去輸出答案。 ```cpp= int arr[N]={}; bool vis[N]={}; int n; void dfs(int index){ } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); dfs(1); } ``` ---- 第一步也是輸入,排序,呼叫函式 第二步理所當然是跑每一項,繼續呼叫遞迴函式 第三步就是回朔,其中也不能忘記在抵達n後要輸出答案 ---- 第一步就經典起手 ```cpp= int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); dfs(1); } ``` ---- 然後跑每一項的時候繼續呼叫遞迴函式,且在結束後要記得回朔。 ```cpp= vis[index]=1; dfs(index+1); vis[index]=0; dfs(index+1); ``` ---- 最後在抵達上限的時候輸出 ```cpp= if(index>n){ for(int i=1;i<=n;i++){ if(vis[i]) cout<<arr[i]<<" "; } return; } ``` ---- ```cpp #include<algorithm> #include<iostream> using namespace std; const int N=1e5+5; int vis[N]={}; int arr[N]={}; int n; void dfs(int index){ if(index>n){ for(int i=1;i<=n;i++){ if(vis[i]) cout<<arr[i]<<" "; } cout<<"\n"; return; } vis[index]=1; dfs(index+1); vis[index]=0; dfs(index+1); } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); dfs(1); return 0; } ``` ---- ## $n$ 個元素枚舉大小為 $k$ 的子集 其實只需要多傳一個參數,代表現在入隊的人有幾個。 $DFS(x,y)$ 表 現在的index $x$ 以及 現在已有 $y$ 個元素入隊 ```cpp= int main(){ cin>>n>>k; if(k>n){ cout<<"-1\n"; return 0; } for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); dfs(1,0); return 0; } ``` ---- 實作內容 : 須注意的點是遞迴函數傳遞的內容以及遞迴函數的停止條件,不然很容易無窮迴圈。 ```cpp= void dfs(int index,int now){ if(now>k||index>n+1) return; // 因為只有剛好恰等於k時輸出 if(now==k){ // 恰等於時輸出 for(int i=1;i<=n;i++){ if(vis[i]) cout<<arr[i]<<" "; } cout<<"\n"; return; } vis[index]=1; dfs(index+1,now+1); // 若有選取 則index+1且元素入隊所以now+1 vis[index]=0; // 回朔 dfs(index+1,now); // 沒有則只+index } ``` ---- ```cpp #include<algorithm> #include<iostream> #include<vector> using namespace std; const int N=1e5+5; int vis[N]={}; int arr[N]={}; int n,k; void dfs(int index,int now){ if(now>k||index>n+1) return; // 因為只有剛好恰等於k時輸出 if(now==k){ // 恰等於時輸出 for(int i=1;i<=n;i++){ if(vis[i]) cout<<arr[i]<<" "; } cout<<"\n"; return; } vis[index]=1; dfs(index+1,now+1); // 若有選取 則index+1且元素入隊所以now+1 vis[index]=0; // 回朔 dfs(index+1,now); // 沒有則只+index } int main(){ cin>>n>>k; if(k>n){ cout<<"-1\n"; return 0; } for(int i=1;i<=n;i++) cin>>arr[i]; sort(arr+1,arr+n+1); dfs(1,0); return 0; } ``` --- # 回顧上週題目
{"slideOptions":"{\"transition\":\"fade;\"}","title":"Struct and Recursive","contributors":"[{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":9779,\"del\":1232},{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":420,\"del\":6}]","description":"結構體 (struct)"}
    502 views
   Owned this note