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 | ❌ |
最后进入栈的数据最先被取出(这种行为被叫做 LIFO: Last In First Out,即后进先出)
最初放入的元素最先被取出(这种行为被叫做 FIFO: First In First Out,即先进先出)
DFS, Depth-First Search
给定整数 a 1 , a 2 , ⋯ , a n a_1, a_2, \cdots, a_n a1,a2,⋯,an,判断是否可以从中选出若干数,使它们的和恰好为 k。
#include
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
*/
从 a 1 a_1 a1 开始按顺序决定每个数加或不加,在全部 n 个数都决定后再判断它们的和是不是 k 即可,复杂度是 O ( 2 n ) O(2^n) O(2n)。
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) O(8×N×M=O(N×M)。
BFS, Breadth-First Search
// 使用 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) O(4×N×M)=O(N×M)。
宽度优先搜索中,只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。这个问题中由于要求最短距离,不妨用 d[N][M]
数组把最短距离保存起来。初始时用充分大的常数 INF 来初始化它,这样尚未到达的位置就是 INF,也就同时起到了标记的作用。
因为要向 4 个不同方向移动,用 dx[4]
和 dy[4]
两个数组来表示四个方向向量。这样通过一个循环就可以实现四方向移动的遍历。
宽度优先搜索与深度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时使用宽度优先搜索也是可以的。但是递归函数可以很简短地编写,而且状态的管理也更简单,所以大多数情况下还是用深度优先搜索实现。反之,在求取最短路时深度优先搜索需要反复经过同样的状态,所以此时还是使用宽度优先搜索为好。
宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间。反之,深度优先搜索是与最大的递归深度成正比的。一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存。
虽然生成可行解空间多数采用深度优先搜索,但在状态空间比较特殊时其实可以很简短地实现。比如,C++ 的标准库中提供了 next permutation
这一函数,可以把 n 个元素共 n! 种不同的排列生成出来。又或者,通过使用位运算,可以枚举从 n 个元素中取出 k 个的共 C n k C_n^k Cnk 种状态或是某个集合中的全部子集等。3.2节将介绍如何利用位运算枚举状态。
专栏 栈内存和堆内存
调用函数时,主调的函数所拥有的局部变量等信息需要存储在栈内存区;
利用 new 或者 malloc 进行分配的内存区域被称为堆内存。
栈内存在程序启动时被统一分配,此后不能再扩大。由于这一区域有上限,所以函数的递归深度也有上限。虽然与函数中定义的局部变量的数目有关,不过一般情况下 C 和 C++ 中进行上万次的递归是可以的。在 Java 中,在执行程序时可以用参数指定栈的大小。
全局变量被保存在堆内存区。通常不推荐使用全局变量,但是在程序设计竞赛中,由于函数通常不是那么多,并且常常会有多个函数访问同一个数组,因此利用全局变量就很方便。此外,有时必须要申请巨大的数组,与放置在栈内存上相比,将其放置在堆内存上可以减少栈溢出的危险。同时,通常只需定义满足最大需要的数列大小,但如果再额外定义大一些,能很好地避免粗心导致的诸如忘记保留字符串末尾的’\0’的空间之类的漏洞。
Page | 思想 | 复杂度 |
---|---|---|
3 | 4 层循环 | O ( n 4 ) O(n^4) O(n4) |
22 | 对原数组排序 3 层循环,二分查找 m - a[i] - a[j] - a[k] |
O ( n log n ) O(n \log n) O(nlogn) O ( n 3 log n ) O(n^3 \log n) O(n3logn) |
23 | 求 a[i] + a[j] 并排序两层循环,二分查找 m - a[i] - a[j] |
O ( n 2 log n ) O(n^2 \log n) O(n2logn) O ( n 2 log n ) O(n^2 \log n) O(n2logn) |
考虑 O ( n 2 ) O(n^2) O(n2) 时间的算法,假设题目描述中的限制条件为 n ≤ 1000 n \leq 1000 n≤1000,将 n = 1000 n = 1000 n=1000 代入 n 2 n^2 n2 就得到了 100000。在这个数值的基础上,我们就可以结合下表进行判断了。
假设时间限制为1秒
1000000 | 游刃有余 |
---|---|
10000000 | 勉勉强强 |
100000000 | 很悬,仅限循环体非常简单的情况 |