<style>
h2{
color:#8B4746;
}
.blue{
color:#4A919E
}
</style>
<font color="#4A919E">DSA 第八週講義</font>
===
>[name= 沈奕呈、林德恩][time=Mar 11,2022 ]
###### tags:`DSA` `Data Structure and Algorithm` `資料結構` `演算法` `Data Structure` `Algorithm` `tcirc39th` `社課` `臺中一中電研社`
[TOC]
---
## <div class="blue">電研社</div>
社網:[tcirc.tw](https://tcirc.tw)
online judge:[judge.tcirc.tw](https://judge.tcirc.tw)
IG:[TCIRC_39th](https://www.instagram.com/tcirc_39th)
---
## 資料結構 (Data Structure)
<!-- https://www.csie.ntu.edu.tw/~r95116/CA200/slide/C6_DataStructure.pdf -->
### 介紹
資料結構是在電腦中用於儲存、組織資料的方式
----
### 常見資料結構種類
堆疊(Stack)
佇列(Queue)
陣列(Array)
連結串列(Linked List)
樹(Tree)
堆積(Heap)
圖(Graph)
雜湊表(Hash table)
<!--維基百科-->
---
## 樹 (tree)
<!-- https://www.csie.ntu.edu.tw/~r95116/CA200/slide/C10_Tree.pdf -->
樹是一種電腦的資料結構,如同容器一樣可儲存資料,並有效地插入,搜尋資料
![](https://i.imgur.com/gfch3G6.png =600x)
[image link](https://www.google.com/url?sa=i&url=https%3A%2F%2Ftowardsdatascience.com%2F8-useful-tree-data-structures-worth-knowing-8532c7231e8c&psig=AOvVaw17gUi1A3LzTZQL70FFlEaX&ust=1636726156512000&source=images&cd=vfe&ved=0CAsQjRxqFwoTCPCQpKK-kPQCFQAAAAAdAAAAABAD)
----
### 名詞介紹
**Root**:根節點,最上面的一個點
**Node**:節點,相連的點
**Parent**:父節點
**Child**:子節點
**Grandchild**:子節點的子節點
**Ancestor**:祖先節點,在往上跨一代的節點都是該點的Ancestor(不含父節點)
**Descendant**:在往下跨一代的節點都是該點的Descendant(不含子節點)
----
**Subtree**:子樹,下面有child的child
**Leaves**:葉節點,沒有child的子節點
**Non-terminal Nodes**:非終端節點,除了葉節點之
外的其它節點稱為非終端節點
**Key**:裡面的值
**Edge**:節點之間的連接
**Sibling**:兄弟節點
**Level**:階層,由上往下算(ex.Level 1,Level 2,Level 3...)
**Hight**:樹高/樹深,由下往上算有幾層
**Degree**:分支度,子節點數
***n*元樹**:該樹的一個節點最多擁有*n*個子節點
----
### 二元樹(binary tree)
二元樹是樹的其中一種結構,它只有每一層只有兩個分支
習慣上認為兩個子樹是不同的tree(只有在二元樹)
----
完滿二元樹(Full Binary Tree):每一層都是最大節點數
完整二元樹(Complete Binary Tree):是完美二元樹或除了最後一層以外是滿的,如果有缺少,一定從右邊開始缺,並且是連續。
Binary Max Tree:根節點$\ge$其子節點,並且其子樹也符合此兩個條件
Binary Mini Tree:根節點$\le$其子節點,並且其子樹也符合此兩個條件
----
Complete Binary Tree
![](https://i.imgur.com/VKQMQGV.gif =x400)
----
### 二元搜尋樹(BST ,Binary Search Tree)
![](https://i.imgur.com/JYg3C6i.png)
[image link](https://www.google.com/url?sa=i&url=https%3A%2F%2Fithelp.ithome.com.tw%2Farticles%2F10205875&psig=AOvVaw1vT_qOt_Jj5MerGNK2x9ma&ust=1636726227746000&source=images&cd=vfe&ved=0CAsQjRxqFwoTCPj3_8K-kPQCFQAAAAAdAAAAABAD)
----
二元搜尋樹一定是排序過後的樹
排序後右邊的樹一定比左邊大,右子樹比左子樹大
時間複雜度是$O(h)$
如果又同時是complete tree,其時間複雜度是$O(log_2n)$
----
#### BST的插入和刪除
![](https://i.imgur.com/INYKqtL.gif =x250)
![](https://i.imgur.com/VACzUNC.gif =x250)
----
實作
----
### 二元樹的走訪
#### 中序(in-order)
先從左邊,接著自己,再接右邊
![](https://i.imgur.com/ZU7VO6S.png)
[來源](https://commons.wikimedia.org/wiki/File:Binary_search_tree.svg)
中序遍歷排成
`1、3、4、6、7、8、10、13、14`
中序遍歷後的BST一定是按照大小
----
#### 前序(pre-order)
先從自己,接著左邊,再接右邊
![](https://i.imgur.com/ZU7VO6S.png)
[來源](https://commons.wikimedia.org/wiki/File:Binary_search_tree.svg)
前序遍歷排成
`8、3、1、6、4、7、10、14、13`
----
#### 後序
先從左邊,接著右邊,再接自己
![](https://i.imgur.com/ZU7VO6S.png)
[來源](https://commons.wikimedia.org/wiki/File:Binary_search_tree.svg)
後序遍歷排成
`1、4、7、6、3、13、14、10、8`
二元樹只要有**前序或後序**及**中序**就能還原
----
### 儲存
#### 陣列儲存
根節點是0
左子樹是父節點編號乘以2
右子樹是父節點編號乘以2加1
如果該編號位置是空的,陣列也要空下來
優點:操作容易,要找每個節點都很容易
缺點:在非完滿二元樹的儲存效率較低
----
#### 鏈結儲存
<!-- -->
在每個節點儲存自己的資料、左節點和右節點的指標
優點:在儲存歪斜樹之類的樹用的空間比陣列小
缺點:在儲存葉節點會浪費未用到的兩個指標的空間(值為NULL)
<!--
```
struct binary_tree_node
{
資料型態 變數名稱;
struct binary_tree_node *left;
struct binary_tree_node *right;
};
typedef struct binary_tree_node node;
node *root;
root = (node *)malloc(sizeof(node));
root ->left= NULL;
root ->right= NULL;
```-->
<!-- https://youtu.be/QzO0rag6geA?list=PLil-R4o6jmGiDc1CC8PyBbasl8kR9r8Wr&t=3400 -->
---
## heap
----
### 舉例
#### max-heap
具有max tree和complete binary tree
#### mini-heap
具有mini tree和complete binary tree
#### binomial heap
----
### max-heap和mini-heap的刪除
刪除後將最後一項補到刪除的位置,再移動到符合max或mini的位置
時間複雜度是$O(log_2n)$
----
### max-heap和mini-heap的插入
將插入的值放到最後,再移動到符合max或mini的位置
時間複雜度是$O(log_2n)$
<!--https://youtu.be/RjvhXL0WTrY?t=8389 -->
<!-- ---
## Hash Table
可以將資料量減少
先將資料進行運算再壓縮,因為進行壓縮,所以有機會有一些會重複,稱之為碰撞,hash function越複雜,越難看出原本的值,也可以用於使資料難以知道原值,如:
比如說hash table -->
<!-- https://youtu.be/SBQLkYIDAZI?t=7364 -->
<!-- https://www.youtube.com/watch?v=WX-z52QweUs -->
---
## 列舉
列舉就是**跑過所有可能的情況**的方法,面對**資料量很小**的演算法問題,可以用列舉簡單暴力的解決
適時使用列舉,能幫你在比賽中**省下不少思考時間**;
不過這個方法遇到資料量大的題目就沒轍了。
----
### 優化/剪枝
根據列舉的方式不同,程式所需的時間複雜度可能會大大的不同
如果能善用優化和剪枝的技巧,將能增加通過題目的機會
- **優化**:減少時間複雜度的級別
ex.拆掉一層或一個迴圈
- **剪枝**:減少不必要的細節
ex.符合題目要求後跳出迴圈,而不是等迴圈跑完
---
### 列舉全排列 O(n!)
題目:[c028: 最短貨物運送距離](https://judge.tcirc.tw/ShowProblem?problemid=c028)
題目說明:在二維座標中,從原點出發經過四個站點,求最短總距離及走訪順序
輸入量:4
----
首先,四個站點兩兩之間的距離有的長有的短,所以「**走訪的順序**」會影響到行走的總距離
如果我們從四個點中挑一個當作第一站,再從剩下三個點中挑一個當作第二站...走訪順序就會有4!種方式
才四個站點,別多想了,就把所有可能列舉一遍吧
----
#### 時間複雜度
列舉全排列的時間複雜度為 $O(n!)$,
因為輸入的資料量 n <=10 ,使用這個方法不會TLE
----
![](https://i.imgur.com/gtomfUB.png =x300)
----
#### 列舉方法
如何列舉出所有走訪順序?
>分別列舉出a.b.c.d站當第一站的走訪順序
如何列舉出a站當第一站的走訪順序?
>分別列舉出b.c.d站當第第二站的走訪順序
---
「<font color="ff0000">在我們解決問題之前,得要先處理另一個子問題
在解決子問題之前,得要先處理另一個子子問題</font>」
-- 這就是遞迴的核心概念
而因為總共只有四個站點,這個遞迴的停止條件便是「列舉第五個站點」
----
#### 函式設計
1.預期作用:列舉第i個站點
2.設定終止條件:i==5
3.計算、比較最短距離及選點順序
4.用「迴圈」列舉a.b.c.d當第i個站點的情況
5.用陣列紀錄目前選點順序
6.迴圈裡面呼叫「函式本身」,先去列舉a.b.c.d當第i+1個站點的情況
----
宣告後面會用到的變數和函式(等後面用到再回來看)
```cpp=
#include <bits/stdc++.h>//萬用標頭檔
using namespace std;
const int max_n=4;
const int max_v=1e6;//1.4*2*1e6 < 2e9
const int inf=1e7;
struct pos{int x,y;};
pos item[5];//以struct紀錄四個點的x.y座標
double ans_dis,t_dis;//目前最短總距離;目前總距離
int ord[2][5];//ord[0][i]為目前最短順序,ord[1][i]為當前順序
bool vis[5]={};//紀錄點有沒有被走過
double dis(pos a,pos b){//計算距離
return sqrt( pow(a.x-b.x,2)+pow(a.y-b.y,2) );
}
```
----
遞迴函式
```cpp=
void recursion(int t){
//終止條件
if(t==5){
t_dis=0;
for(int i=1;i<=4;i+=1){
t_dis+=dis( item[ ord[1][i] ] , item[ ord[1][i-1] ] );
}
if(t_dis<ans_dis){//字典序較大者,總距離一定得更小
ans_dis=t_dis;//更新距離、順序
for(int i=1;i<=4;i+=1){
ord[0][i]=ord[1][i];
}
}
return;
}
//recursion
for(int i=1;i<=4;i+=1){
if(vis[i]==1) continue;
vis[i]=1;//挑第幾個點
ord[1][t]=i;//第幾次挑的點
recursion(t+1);
vis[i]=0;//取消挑這個點,換下個點
}
}
```
----
主程式
```cpp=
int main() {
//init
item[0]={0,0};//原點
ord[0][0]=0;
ord[1][0]=0;
//預設值要比可能的答案都還要大,才能找到越來越小的答案
ans_dis=inf;
//cin
for(int i=1;i<=4;i+=1){
cin>>item[i].x>>item[i].y;
}
//列舉
recursion(1);
//cout
//加上「<<fixed<< setprecision(2)」取兩位小數
cout<<fixed<< setprecision(2)<<ans_dis<<'\n';
for(int i=0;i<=4;i+=1)
cout<<ord[0][i];
return 0;
}
```
---
### 子集合列舉 $O(2^n)$
題目:[d007: 習題 Q-1-8. 子集合的和](https://judge.tcirc.tw/ShowProblem?problemid=d007)
題目說明:在一個數字集合內挑一些數字,使選出的數字集合最接近P且不超過P
輸入量:25
----
每個數字有**選與不選**兩種可能,只要列舉出所有情況的數字總和,就能找到最接近P且不超過P的數字總和
總共有 $2^{25}$ 種情況呢!
還算可以,先試試看吧
----
#### 時間複雜度
列舉全排列的時間複雜度為 $O(2^n)$,
因為輸入的資料量 n <=25 ,使用這個方法還不會TLE,n到30大概就TLE惹
----
#### 列舉方法
如何列舉出所有情況?
>分別列舉出第一個數字選與不選的情況
如何列舉出第一個數字選與不選的情況?
>分別列舉出第二個數字選與不選的情況
你很聰明😏,又是遞迴
----
#### 函式設計
1.預期作用:列舉第x個數字選與不選兩種情況
2.設定其一終止條件:x>=n
3.不選數字,呼叫「函式本身」,先去列舉第x+1個數字選與不選兩種情況
4.設定其二終止條件:數字總和超過P
5.選數字,與目前最大數字和比較
6.呼叫「函式本身」,先去列舉第x+1個數字選與不選兩種情況
----
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int max_n=25;
const int max_p=1e9+9;//p、num、ans可用int
long long a[25];//數字大小沒限制
int n,p,num,ans;
void recursion(int x){
if(x>=n) return;//終止條件1
recursion(x+1);//不選第x個數字
if(num+a[x]>p) return;//終止條件2
num+=a[x];//選第x個數字
ans=max(ans,num);//因數字總和變大,與目前最大數字和比較
recursion(x+1);
num-=a[x];//復原沒有選過的狀態
}
int main(){
cin>>n>>p;
for(int i=0;i<n;i+=1)
cin>>a[i];
ans=0; num=0;
recursion(0);
cout<<ans<<'\n';
return 0;
}
```
---
### 二維陣列列舉 $O(n^2)$
題目:[atcoder:abc241_c](https://atcoder.jp/contests/abc241/tasks/abc241_c)
題目說明:在一個二維陣列裡面有 ``#`` 和 `.`兩種字元,如果將其中至多兩個 `.` 改成 `#` ,能將六個 `#`連成一線(直橫斜都可),輸出"Yes",否則輸出"No"
輸入量:1000
----
如果在二維陣列所有長度為6的直橫斜線中,有一條含有至少四個 '#',
就可以透過將剩下的 '.' 改成 '#' ,讓六個 '#'連成一線
而列舉二維陣列的每一點,往八個方向延伸,就等於是列舉出每一條長度為6的直橫斜線惹~
----
「在二維陣列裡藉由各種方式延伸,查看有無符合題目要求」,這種題目蠻常見的,資料量小的可以用雙層迴圈列舉( $O(n^2)$ ),資料量大的則需使用特定演算法
----
#### 列舉方法:
用雙層迴圈列舉二維陣列裡的每一個點當起點,往八個方向向外延伸五格,如果某個方向的六格皆在二維陣列內,且其中有至少四個 '#',就能符合題目要求了
----
#### 時間複雜度
二維陣列列舉的時間複雜度為 $O(n^2)$,
因為輸入的資料量遠小於5000,放心的列舉二維陣列的每一格吧
----
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int max_n=1e3;
int n,flag;
//上、下、左、右、左上、右上、左下、右下
int dr[8]{-1,1, 0,0,-1,-1, 1,1};//上下
int dc[8]{ 0,0,-1,1,-1, 1,-1,1};//左右
string block[max_n];
void line(int r, int c,int k){
int temp=0;
for(int l=0;l<6;l+=1){
if(r>=n || c>=n || r<0 || c<0) break;
if(block[r][c]=='#') temp+=1;
if(l==5 and temp>=4) flag=1;
r+=dr[k];
c+=dc[k];
}
return;
}
int main() {
cin>>n;
for(int i=0;i<n;i+=1)
cin>>block[i];
flag=0;
for(int r=0;r<n;r+=1){
for(int c=0;c<n;c+=1){
for(int k=0;k<8;k+=1)
line(r,c,k);
}
}
(flag)? cout<<"Yes" : cout<<"No";
return 0;
}
```
----
#### 剪枝方向:
1. 函式跑完後如果flag=1後跳出迴圈
2. 因為是由左上往右下列舉,只需往「右、右下、下、左下」四個方向沿伸
因為時間很夠所以就不示範剪枝版本了
---
### 折半列舉
題目:[d019: 例題 Q-2-10. 子集合的和(折半枚舉)](https://judge.tcirc.tw/ShowProblem?problemid=d019)
題目說明:在一個數字集合內挑一些數字,使選出的數字集合最接近P且不超過P
輸入量:38
----
好欸,一樣的題目!!
等等...剛才的方法n到25不是極限了嗎,這題的n居然是38!?
如果把38個數分成兩半,分別列舉兩邊所有情況的數字總和後(n=19),再從兩邊(或一邊)各取一數相加是不是就可以了?
----
1. 分別列舉兩邊「所有情況」的「數字總和」S(A)、S(B) 並記錄
$O(2^{n/2})$+$O(2^{n/2})$
2. 分別列舉S(A)、S(B)裡的元素相加
$2^{n/2}$個*$2^{n/2}$個=$O(2^n)$
時間複雜度=$O(2^{n/2})$+$O(2^{n/2})$+<font color="ff0000">$O(2^n)$</font>
----
這樣沒有比較好啊!!
冷靜一下,第二點是可以優化的
----
A集合取一個數,B集合取一個數,兩者相加找最接近的值
其實有比列舉更快的方法,而且我們用過很多次
EX. x、y是整數,3x+2y<=30,求3x+2y最接近30的數字
遇到題目,我們通常會列舉 3x=3、6、9...的情況
再找出 2y 的情況中找出 <= 30-3x 的最大值
----
前述的3x、2y等同於**題目裡集合S(A)、S(B)**
S(3X)={3,6,9,...,30} ; S(2Y)={2,4,6,...,30}
而前述「找出 2y 中 <= 30-3x 的最大值」
**等同於在S(B)中找出符合( S(B)<=P-S(A) )的最大元素**
這個部分可以用set的upper_bound()實現
----
#### 時間複雜度
1. 分別列舉兩邊「所有情況」的「數字總和」S(A)、S(B) 並記錄
$O(2^{n/2})$+$O(2^{n/2})$
2. 列舉S(A),S(B)用upper_bound()找出S(B)中符合條件的最大值
$O(2^{n/2})$*$O( log2^{n/2} )$
時間複雜度=$O(2^{n/2})$+$O(2^{n/2})$+<font color="ff0000">$O(n)$ * $O(2^{n/2})$</font>=$O(2^{n/2})$
----
#### lower_bound() / upper_bound()
set中有一組方便的函式 `lower_bound() / upper_bound()`
- `.upper_bound(x)` :回傳第一個大於指定的值的元素 iteritor(沒有就回傳.end())
**-> `.*upper_bound(x)` 找大於x的最小元素的值**
- `.lower_bound()` :回傳第一個大於等於指定的值的元素 iteritor(沒有就回傳.end())
**-> `.*lower_bound(x)` 找大於等於x的最小元素的值**
----
如果我們要找出符合( S(B)<=P-S(A) )的最大元素
就把S(B)丟進去multiset,使用`.*(upper_bound(x)-1)` 找小於等於x的最大元素的值
<font color="ff0000">注意1:數字會重複所以要用multiset</font>
<font color="ff0000">注意2:記得要加星號取該元素iterator的值</font>
----
![](https://i.imgur.com/VOBgfdS.png)
----
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int max_n=38;
const long long max_v=1<<60;
long long a[38];
int n;
long long p,num,ans;
vector<long long> sum_a;
multiset<long long> sum_b;
```
----
函式
```cpp=
void SA(int x){
if(x>=n/2) return;
SA(x+1);
if(num+a[x]>p) return;
num+=a[x];
sum_a.push_back(num);
SA(x+1);
num-=a[x];
}
void SB(int x){
if(x>=n) return;
SB(x+1);
if(num+a[x]>p) return;
num+=a[x];
sum_b.insert(num);
SB(x+1);
num-=a[x];
}
```
----
主程式
```cpp=
int main(){
cin>>n>>p;
for(int i=0;i<n;i+=1)
cin>>a[i];
ans=0; num=0;
SA(0);//S(A)
SB(n/2);//S(B)
//折半枚舉
multiset<long long>::iterator it;
if(sum_b.empty()==0){
it=sum_b.end();
it--;
ans=max(ans,*(it));//只選S(B)
}
vector<long long>::iterator begin=sum_a.begin();
vector<long long>::iterator end=sum_a.end();
for(auto sa=begin;sa!=end;sa++){
it=sum_b.upper_bound(p-*(sa) );
//S(B)中所有元素均超過p-S(A)
if(it==sum_b.begin() ){//只選S(A)
ans=max( ans,*(sa) );
continue;
}
it--;
//比較S(A)+S(B)與目前最大數字總和
ans=max( ans,*(it)+*(sa) );
}
//
cout<<ans<<'\n';
return 0;
}
```
{"metaMigratedAt":"2023-06-16T20:00:31.750Z","metaMigratedFrom":"YAML","title":"DSA 第八週講義 臺中一中電研社","breaks":true,"slideOptions":"{\"theme\":\"sky\",\"transition\":\"convex\"}","contributors":"[{\"id\":\"a031de8f-38ef-4123-9d53-e13dd69cbbc3\",\"add\":9439,\"del\":1589},{\"id\":\"bd47cc0a-d3e4-4997-b042-3ae3230b8982\",\"add\":7503,\"del\":3197},{\"id\":\"6a5475c5-bfd3-428c-9219-c760b9000deb\",\"add\":12,\"del\":11}]"}