《挑戰程序設計競賽》第一階段閱讀計劃
Date | Chapter | Start Page | End Page | Finish on Time❓ |
---|---|---|---|---|
2020-07-06 | 2.1 最基礎的“窮竭搜索” | 26 | 38 | ✔️ |
2020-07-07 | 2.2 一往直前!貪心法 | 39 | 50 | |
2020-07-08 | 2.3 記錄結果再利用的“動態規劃” | 51 | 69 | |
2020-07-09 | 2.4 加工並存儲數據的數據結構 | 70 | 90 | |
2020-07-10 | 2.5 它們其實都是“圖” | 91 | 112 | |
2020-07-11 | 2.6 數學問題的解題竅門 | 113 | 124 | |
2020-07-12 | 2.7 一起來挑戰GCJ的題目(1) | 125 | 135 | ❌ |
第 2 章 初出茅廬——初級篇
2.1 最基礎的“窮竭搜索”
2.1.2 棧
最後進入棧的數據最先被取出(這種行爲被叫做 LIFO: Last In First Out,即後進先出)
2.1.3 隊列
最初放入的元素最先被取出(這種行爲被叫做 FIFO: First In First Out,即先進先出)
2.1.4 深度優先搜索
DFS, Depth-First Search
P30 部分和問題
給定整數 ,判斷是否可以從中選出若干數,使它們的和恰好爲 k。
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 20 + 5;
int n;
int a[MAX_N];
int k;
void input() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
scanf("%d", &k);
}
bool dfs(int i, int sum) {
if (i == n) {
return (sum == k);
}
if (dfs(i + 1, sum)) {
return true;
}
if (dfs(i + 1, sum + a[i])) {
return true;
}
return false;
}
int main() {
input();
if (dfs(0, 0)) {
printf("Yes\n");
}
else {
printf("No\n");
}
return 0;
}
/*
Input:
4
1 2 4 7
13
Output:
Yes
Input:
4
1 2 4 7
15
Output:
No
*/
從 開始按順序決定每個數加或不加,在全部 n 個數都決定後再判斷它們的和是不是 k 即可,複雜度是 。
P32 Lake Counting(POJ No.2386)
const int MAX_N = 100 + 5;
const int MAX_M = 100 + 5;
int n;
int m;
char field[MAX_N][MAX_M];
void dfs(int x, int y) {
field[x][y] = '.';
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
int nx = x + dx;
int ny = y + dy;
if (0 <= nx && nx < N && 0 <= ny && ny < M && field[nx][ny] == 'W') {
dfs(nx, ny);
}
}
}
return;
}
void solve() {
int res = 0;
for (int x = 0; x < N; x++) {
for (int y = 0; y < M; y++) {
if (field[x][y] == 'W') {
dfs(x, y);
res++;
}
}
}
printf("%d\n", res);
}
從任意的 w 開始,不停地把鄰接的部分用 ‘.’ 代替。1 次 DFS 後與初始的這個 w 連接的所有 w 就都被替換成了 ‘.’,因此直到圖中不再存在 w 爲止,總共進行 DFS 的次數就是答案了。8 個方向共對應了 8 種狀態轉移,每個格子作爲 DFS 的參數至多被調用一次,所以複雜度爲 。
2.1.5 寬度優先搜索
BFS, Breadth-First Search
P34 迷宮的最短路徑
// 使用 pair 表示狀態時,使用 typedef 會更加方便一些
typedef pair<int, int> P;
const int MAX_N = 100 + 5;
const int MAX_M = 100 + 5;
// 輸入
char maze[MAX_N][MAX_M];
int N, M;
int sx, sy; // 起點座標
int gx, gy; // 終點座標
int d[MAX_N][MAX_N]; // 到各個位置的最短距離的數組
int dx[4] = { 1, 0, -1, 0 };
int dy[4] = { 0, 1, 0, -1 };
int bfs() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
d[i][j] = INF;
}
}
d[sx][sy] = 0;
queue<P> que;
que.push(P(sx, sy));
while (!que.empty()) {
P p = que.front();
que.pop();
if (p.first == gx && p.second = gy) {
break;
}
for (int i = 0; i < 4; i++) {
int nx = p.first + dx[i];
int ny = p.second + dy[i];
if (0 <= nx && nx < N && 0 <= ny && ny <= M && maze[nx][ny] != '#' && d[nx][ny] == INF) {
que.push(P(nx, ny));
d[nx][ny] = d[p.first][p.second] + 1;
}
}
}
return d[gx][gy];
}
寬度優先搜索按照距開始狀態由近及遠的順序進行搜索,因此可以很容易地用來求最短路徑、最少操作之類問題的答案。這個問題中,狀態僅僅是目前所在位置的座標,因此可以構造成 pair 或者編碼成 int 來表達狀態。當狀態更加複雜時,就需要封裝成一個類來表示狀態了。轉移的方式爲四方向移動,狀態數與迷宮的大小是相等的,所以複雜度是 。
寬度優先搜索中,只要將已經訪問過的狀態用標記管理起來,就可以很好地做到由近及遠的搜索。這個問題中由於要求最短距離,不妨用 d[N][M]
數組把最短距離保存起來。初始時用充分大的常數 INF 來初始化它,這樣尚未到達的位置就是 INF,也就同時起到了標記的作用。
因爲要向 4 個不同方向移動,用 dx[4]
和 dy[4]
兩個數組來表示四個方向向量。這樣通過一個循環就可以實現四方向移動的遍歷。
寬度優先搜索與深度優先搜索一樣,都會生成所有能夠遍歷到的狀態,因此需要對所有狀態進行處理時使用寬度優先搜索也是可以的。但是遞歸函數可以很簡短地編寫,而且狀態的管理也更簡單,所以大多數情況下還是用深度優先搜索實現。反之,在求取最短路時深度優先搜索需要反覆經過同樣的狀態,所以此時還是使用寬度優先搜索爲好。
寬度優先搜索會把狀態逐個加入隊列,因此通常需要與狀態數成正比的內存空間。反之,深度優先搜索是與最大的遞歸深度成正比的。一般與狀態數相比,遞歸的深度並不會太大,所以可以認爲深度優先搜索更加節省內存。
2.1.6 特殊狀態的枚舉
雖然生成可行解空間多數採用深度優先搜索,但在狀態空間比較特殊時其實可以很簡短地實現。比如,C++ 的標準庫中提供了 next permutation
這一函數,可以把 n 個元素共 n! 種不同的排列生成出來。又或者,通過使用位運算,可以枚舉從 n 個元素中取出 k 個的共 種狀態或是某個集合中的全部子集等。3.2節將介紹如何利用位運算枚舉狀態。
2.1.7 剪枝
專欄 棧內存和堆內存
調用函數時,主調的函數所擁有的局部變量等信息需要存儲在棧內存區;
利用 new 或者 malloc 進行分配的內存區域被稱爲堆內存。
棧內存在程序啓動時被統一分配,此後不能再擴大。由於這一區域有上限,所以函數的遞歸深度也有上限。雖然與函數中定義的局部變量的數目有關,不過一般情況下 C 和 C++ 中進行上萬次的遞歸是可以的。在 Java 中,在執行程序時可以用參數指定棧的大小。
全局變量被保存在堆內存區。通常不推薦使用全局變量,但是在程序設計競賽中,由於函數通常不是那麼多,並且常常會有多個函數訪問同一個數組,因此利用全局變量就很方便。此外,有時必須要申請巨大的數組,與放置在棧內存上相比,將其放置在堆內存上可以減少棧溢出的危險。同時,通常只需定義滿足最大需要的數列大小,但如果再額外定義大一些,能很好地避免粗心導致的諸如忘記保留字符串末尾的’\0’的空間之類的漏洞。
第 1 章 蓄勢待發——準備篇
1.1 何謂程序設計競賽
Page | 思想 | 複雜度 |
---|---|---|
3 | 4 層循環 | |
22 | 對原數組排序 3 層循環,二分查找 m - a[i] - a[j] - a[k] |
|
23 | 求 a[i] + a[j] 並排序兩層循環,二分查找 m - a[i] - a[j] |
1.5 以高效的算法爲目標
考慮 時間的算法,假設題目描述中的限制條件爲 ,將 代入 就得到了 100000。在這個數值的基礎上,我們就可以結合下表進行判斷了。
假設時間限制爲1秒
1000000 | 遊刃有餘 |
---|---|
10000000 | 勉勉強強 |
100000000 | 很懸,僅限循環體非常簡單的情況 |