《挑戰程序設計競賽》閱讀筆記

《挑戰程序設計競賽》第一階段閱讀計劃

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 部分和問題

給定整數 a1,a2,,ana_1, a_2, \cdots, a_n,判斷是否可以從中選出若干數,使它們的和恰好爲 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
*/

a1a_1 開始按順序決定每個數加或不加,在全部 n 個數都決定後再判斷它們的和是不是 k 即可,複雜度是 O(2n)O(2^n)

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 的參數至多被調用一次,所以複雜度爲 O(8×N×M=O(N×M)O(8 \times N \times M=O(N \times M)

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 來表達狀態。當狀態更加複雜時,就需要封裝成一個類來表示狀態了。轉移的方式爲四方向移動,狀態數與迷宮的大小是相等的,所以複雜度是 O(4×N×M)=O(N×M)O(4 \times N \times M) = O(N \times M)

寬度優先搜索中,只要將已經訪問過的狀態用標記管理起來,就可以很好地做到由近及遠的搜索。這個問題中由於要求最短距離,不妨用 d[N][M] 數組把最短距離保存起來。初始時用充分大的常數 INF 來初始化它,這樣尚未到達的位置就是 INF,也就同時起到了標記的作用。

因爲要向 4 個不同方向移動,用 dx[4]dy[4] 兩個數組來表示四個方向向量。這樣通過一個循環就可以實現四方向移動的遍歷。

寬度優先搜索與深度優先搜索一樣,都會生成所有能夠遍歷到的狀態,因此需要對所有狀態進行處理時使用寬度優先搜索也是可以的。但是遞歸函數可以很簡短地編寫,而且狀態的管理也更簡單,所以大多數情況下還是用深度優先搜索實現。反之,在求取最短路時深度優先搜索需要反覆經過同樣的狀態,所以此時還是使用寬度優先搜索爲好。

寬度優先搜索會把狀態逐個加入隊列,因此通常需要與狀態數成正比的內存空間。反之,深度優先搜索是與最大的遞歸深度成正比的。一般與狀態數相比,遞歸的深度並不會太大,所以可以認爲深度優先搜索更加節省內存。

2.1.6 特殊狀態的枚舉

雖然生成可行解空間多數採用深度優先搜索,但在狀態空間比較特殊時其實可以很簡短地實現。比如,C++ 的標準庫中提供了 next permutation 這一函數,可以把 n 個元素共 n! 種不同的排列生成出來。又或者,通過使用位運算,可以枚舉從 n 個元素中取出 k 個的共 CnkC_n^k 種狀態或是某個集合中的全部子集等。3.2節將介紹如何利用位運算枚舉狀態。

2.1.7 剪枝

專欄 棧內存和堆內存

  • 調用函數時,主調的函數所擁有的局部變量等信息需要存儲在棧內存區;

  • 利用 new 或者 malloc 進行分配的內存區域被稱爲堆內存。

  • 棧內存在程序啓動時被統一分配,此後不能再擴大。由於這一區域有上限,所以函數的遞歸深度也有上限。雖然與函數中定義的局部變量的數目有關,不過一般情況下 C 和 C++ 中進行上萬次的遞歸是可以的。在 Java 中,在執行程序時可以用參數指定棧的大小。

  • 全局變量被保存在堆內存區。通常不推薦使用全局變量,但是在程序設計競賽中,由於函數通常不是那麼多,並且常常會有多個函數訪問同一個數組,因此利用全局變量就很方便。此外,有時必須要申請巨大的數組,與放置在棧內存上相比,將其放置在堆內存上可以減少棧溢出的危險。同時,通常只需定義滿足最大需要的數列大小,但如果再額外定義大一些,能很好地避免粗心導致的諸如忘記保留字符串末尾的’\0’的空間之類的漏洞。

第 1 章 蓄勢待發——準備篇

1.1 何謂程序設計競賽

Page 思想 複雜度
3 4 層循環 O(n4)O(n^4)
22 對原數組排序
3 層循環,二分查找 m - a[i] - a[j] - a[k]
O(nlogn)O(n \log n)
O(n3logn)O(n^3 \log n)
23 a[i] + a[j] 並排序
兩層循環,二分查找 m - a[i] - a[j]
O(n2logn)O(n^2 \log n)
O(n2logn)O(n^2 \log n)

1.5 以高效的算法爲目標

考慮 O(n2)O(n^2) 時間的算法,假設題目描述中的限制條件爲 n1000n \leq 1000,將 n=1000n = 1000 代入 n2n^2 就得到了 100000。在這個數值的基礎上,我們就可以結合下表進行判斷了。

假設時間限制爲1秒

1000000 遊刃有餘
10000000 勉勉強強
100000000 很懸,僅限循環體非常簡單的情況

1.6 輕鬆熱身


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章