回首往事,再来看自己五年前写的这篇文章 《夜深人静写算法(一)- 搜索入门》,发现虽然标题写的是入门,但是还是有很多地方较为晦涩,而且多为文字,图片相对较少,初学者不易理解,这和我当初分享算法的初衷背道而驰。毕竟我不希望很多壮怀激烈的仁人志士因为我的文章放弃了算法这条路。因为,我的愿景是:让天下没有难学的算法。
基于这个原因,我打算重新整理《夜深人静写算法》系列,让同样和我志同道合的人积极投身到这个事业中来,将祖国的算法和技术发扬光大,背靠祖国,面向国际,强我国威,壮我河山!
这次的修订版本会持续连载,并且会从易到难重新整理规划目录,读者可以放心从前往后阅读,如果中途有涉及到一些数学、数据结构、计算机原理方面的知识,我会另外开辟章节进行详细讲解。当然,如果有遇到写的不清楚的地方,也可以给我留言,我会尽我最大的努力把它调整清晰,文章群体主要面向 中小学生以及大学生,毕竟,你们才是国家未来的栋梁之才!
1)算法原理
选择一个起始点 u 作为 当前结点,执行如下操作:
a. 访问 当前结点,并且标记该结点已被访问,然后跳转到 b;
b. 如果存在一个和 当前结点 相邻并且尚未被访问的结点 v,则将 v 设为 当前结点,继续执行 a;
c. 如果不存在这样的 v,则进行回溯,回溯的过程就是回退 当前结点;
【例题1】给定一个 n 个结点的无向图,要求从 0 号结点出发遍历整个图,求输出整个过程的遍历序列。其中,遍历规则为:
1)如果和 当前结点 相邻的结点已经访问过,则不能再访问;
2)每次从和 当前结点 相邻的结点中寻找一个编号最小的没有访问的结点进行访问;
图三-1
0 -> 1 -> 3 -> 4 -> 5 -> 2 -> 6
2)算法实现
const int MAXN = 7;
void dfs(int u) {
if(visit[u]) {
// 1
return ;
}
visit[u] = true; // 2
dfs_add(u); // 3
for(int i = 0; i < MAXN; ++i) {
int v = i;
if(adj[u][v]) {
// 4
dfs(v); // 5
}
}
}
visit[MAXN]
数组是一个bool数组,用于标记某个节点是否已访问,初始化都为 false;这里对已访问结点执行回溯;visit[u] = true;
对未访问结点 u 标记为已访问状态;dfs_add(u);
用来将 u 存储到的访问序列中,实际函数实现如下:void dfs_add(int u) {
ans[ansSize++] = u;
}
adj[MAXN][MAXN]
是图的邻接矩阵,用 0 或 1 来代表点是否连通,对于上面的例子,邻接矩阵表示如下:bool adj[MAXN][MAXN] = {
{
0, 1, 1, 0, 0, 0, 0},
{
1, 0, 0, 1, 1, 0, 0},
{
1, 0, 0, 0, 0, 1, 1},
{
0, 1, 0, 0, 0, 0, 0},
{
0, 1, 0, 0, 0, 1, 0},
{
0, 0, 1, 0, 1, 0, 0},
{
0, 0, 1, 0, 0, 0, 0},
};
(adj[u][v] = 1
代表 u 和 v 之间有一条有向边;adj[u][v] = 0
代表没有边)
3)基础应用
a. 求阶乘
【例题2】给出 n ( n < = 10 ) n ( n <= 10 ) n(n<=10),求 n ! = n ∗ ( n − 1 ) ∗ . . . ∗ 3 ∗ 2 ∗ 1 n! = n*(n-1)*...*3*2*1 n!=n∗(n−1)∗...∗3∗2∗1;
int dfs(int n) {
return !n ? 1 : n * dfs(n-1);
}
(由于 C++ 中的 int 是 32 位整数,最大能够表示的值为 2 31 − 1 2^{31}-1 231−1,所以这里的 n 太大就会导致溢出,需要用数组来模拟实现高精度,这个也会在后面的章节来详细讲解如何实现一个高精度的四则运算)
b. 求斐波那契数列的第n项
【例题3】令 g ( n ) = g ( n − 1 ) + g ( n − 2 ) g(n) = g(n-1) + g(n-2) g(n)=g(n−1)+g(n−2), ( 1 < n < 40 ) (1 < n < 40) (1<n<40),其中 g ( 0 ) = g ( 1 ) = 1 g(0) = g(1) = 1 g(0)=g(1)=1;
int dfs(unsigned int n) {
if(n <= 1) {
return 1;
}
return dfs(n-1) + dfs(n-2);
}
c. 求 n 个数的全排列
【例题4】给定 n n n, 按字典序输出 1 到 n n n 的所有全排列;
void dfs(int depth) {
// 1
if(depth == MAXN) {
// 2
dfs_print();
return;
}
for(int i = 1; i <= MAXN; ++i) {
int v = i;
if(!visit[v]) {
// 3
dfs_add(v); // 4
dfs(depth+1);
dfs_dec(v);
}
}
}
depth
参数用来做计数用,表明本次遍历了多少个结点;MAXN
个的时候,输出访问的元素列表;visit[v]
用来判断 v v v 这个元素是否有访问过;dfs_add
和 dfs_dec
分别表示将结点从访问列表加入和删除;void dfs_add(int u) {
visit[u] = true;
ans[ansSize] = u;
++ansSize;
}
void dfs_dec(int u) {
--ansSize;
visit[u] = false;
}
4)高级应用
a. 枚举
b. 容斥原理
c. 基于状态压缩的动态规划
d.记忆化搜索
e.有向图强连通分量
f. 无向图割边割点和双连通分量
g. LCA 最近公共祖先
h.博弈
i.二分图最大匹配
j.欧拉回路
k.K短路
l. 线段树
m. 最大团
n. 最大流
o. 树形DP
1)算法原理
【例题5】如图三-7所示,图中的橙色小方块就是传说中的作者,他可以在一个 n*m 的棋盘上行走,但是只有两个方向,一个是向右,一个是向下(如绿色箭头所示),棋盘上有很多的金矿,走到格子上就能取走那里的金矿,每个格子的金矿数目不同(用蓝色数字表示金矿的数量),问作者在这样一个棋盘上最多可以拿到多少金矿。
图三-7
2)算法实现
int dfs(int i, int j) {
if(i == 0 && j == 0) {
// 1
return D[0][0] = gold[0][0];
}
if(i < 0 || j < 0) {
// 2
return 0;
}
if(D[i][j] != -1) {
// 3
return D[i][j];
}
return D[i][j] = gold[i][j] + max(dfs(i-1,j), dfs(i, j-1)); // 4
}
1)当 i i i 和 j j j 都为 0,代表起点,直接返回起点的金矿值;
2)当 i < 0 i < 0 i<0 或者 j < 0 j < 0 j<0 时, 代表是个不合法的点,则直接返回最小值 0;
3)当 D [ i ] [ j ] D[i][j] D[i][j] 不等于默认值 -1 时,说明之前已经通过其他途径遍历到 ( i , j ) (i,j) (i,j) 这个点并且已经计算过最优值,可以直接返回。
4)利用递归计算 ( i , j ) (i,j) (i,j) 这个点的最优值,并且赋值给 D [ i ] [ j ] D[i][j] D[i][j] 作为记忆化。
样例的计算结果如下:
0 0 0 1 1 1
0 0 2 2 7 7
0 3 3 8 8 8
1 3 5 8 8 8
1)算法原理
a. 正确性
b. 准确性
c. 高效性
2)可行性剪枝
【例题6】如图三-9所示,问作者能否在正好第 11 秒的时候避过各种障碍物(图中的东西一看就知道哪些是障碍物了,_)最终取得爱心,作者每秒能且只能移动一格,允许走重复的格子。
图三-9
3)最优性剪枝(上下界剪枝)
1)算法原理
迭代加深算法原理如下:
1、枚举深度。
2、根据限定的深度进行 dfs,并且利用估价函数进行剪枝。
2)算法实现
void IDA_Star(int startState) {
int maxDepth = 0;
while (true) {
if(dfs(startState, 0, maxDepth)) {
return ;
}
maxDepth = maxDepth + 1;
}
}
3)简单举例
【例题7】如图三-10所示,一个“井”字形的玩具,上面有三种数字1、2、3,给出8种操作方式,A表示将第一个竖着的列循环上移一格,并且A和F是一个逆操作,B、C、D…的操作方式依此类推,初始状态给定,目标状态是中间8个数字相同。问最少的操作方式,并且要求给出操作的序列,步数一样的时候选择字典序最小的输出。图中的操作序列为AC。
图三-10
1)算法原理
选择一个起始点 u 放入一个先进先出的队列中,执行如下操作:
a. 如果队列不为空,弹出一个队列首元素,记为 当前结点,执行b;否则算法结束;
b. 将与 当前结点 相邻并且尚未被访问的结点的信息进行更新,并且全部放入队列中,继续执行a;
【例题8】给定一个无向连通图,如果两个结点间有直接的边那么权值就是1,求从1号结点出发,到所有点的最短距离。
图四-1
2)算法实现
const int inf = -1;
void bfs(int u) {
queue <int> q;
memset(dis, inf, sizeof(dis)); // 1
dis[u] = 0;
q.push(u);
while(!q.empty()) {
u = q.front(); // 2
q.pop();
for(int v = 1; v <= n; ++v) {
if(!adj[u][v]) continue; // 3
if(dis[v] != inf) continue; // 4
dis[v] = dis[u] + 1; // 5
q.push(v);
}
}
}
dis[u]
数组用来标记从起点到 u 的最短距离,初始化为 -1 代表无限远;adj[u][v]=0
代表不连通,则直接跳过;dis[v] != inf
代表 v 这个结点已经访问过,则直接跳过;3)基础应用
a. 最短路
b. 拓扑排序
c. FloodFill
4)高级应用
a. 差分约束
b. 稳定婚姻
c. AC自动机
d. 矩阵二分
e. 基于k进制的状态压缩搜索
f. 基于A*的广度优先搜索
g.双向广搜
本文所有示例代码均可在以下 github 上找到:github.com/WhereIsHeroFrom/模板/图论
题目链接 | 难度 | 解法 |
---|---|---|
Red and Black | ★☆☆☆☆ | FloodFill |
The Game | ★☆☆☆☆ | FloodFill |
Frogger | ★☆☆☆☆ | 二分枚举答案 + FloodFill |
Nearest Common Ancestors | ★☆☆☆☆ | 最近公共祖先 |
Robot Motion | ★☆☆☆☆ | 递归模拟 |
Dessert | ★☆☆☆☆ | 枚举 |
Matrix | ★☆☆☆☆ | 枚举 |
Frame Stacking | ★☆☆☆☆ | 枚举 |
Transportation | ★☆☆☆☆ | 枚举 |
Pairs of Integers | ★★☆☆☆ | 枚举 |
方程的解数 | ★★☆☆☆ | 枚举 + 散列HASH |
Maze | ★★☆☆☆ | 建完图后FloodFill |
Trees Made to Order | ★★☆☆☆ | 递归构造解 |
Cycles of Lanes | ★★☆☆☆ | 简单图最长环 |
The Settlers of Catan | ★★☆☆☆ | 简单图最长链 |
Parity game | ★★☆☆☆ | 简单图判奇环(交错染色) |
Increasing Sequences | ★★☆☆☆ | 枚举 |
Necklace Decomposition | ★★☆☆☆ | 枚举 |
Rikka with Tree | ★★☆☆☆ | 统计 |
Mahjong tree | ★★★☆☆ | 统计 |
Machine Schedule | ★★★☆☆ | 二分图最大匹配 |
Chessboard | ★★★☆☆ | 棋盘覆盖问题 |
Air Raid | ★★★☆☆ | DAG图 最小路径覆盖 |
Entropy | ★★★☆☆ | 枚举 + 适当剪枝 |
Dropping the stones | ★★★☆☆ | 枚举 + 适当剪枝 |
Dreisam Equations | ★★★☆☆ | 表达式求值 |
Firefighters | ★★★☆☆ | 表达式求值 |
Cartesian Tree | ★★★☆☆ | 笛卡尔树的构造 |
Binary Stirling Numbers | ★★★☆☆ | 分形 |
Obfuscation | ★★★☆☆ | 字符串匹配 |
Graph Coloring | ★★★☆☆ | 最大团 |
Pusher | ★★★☆☆ | 暴力搜索 |
Self-Replicating Numbers | ★★★★☆ | 枚举 |
Last Digits | ★★★★☆ | DFS + 欧拉函数 |
Secret Code | ★★★★☆ | 实复数进制转化 |
Anniversary Cake | ★★★★☆ | 矩形填充 |
A Puzzling Problem | ★★★★☆ | 枚举摆放 |
Vase collection | ★★★★☆ | 图的完美匹配 |
Packing Rectangles | ★★★★☆ | 枚举摆放 |
Missing Piece 2001 | ★★★★☆ | N*N-1 数码问题,强剪枝 |
题目链接 | 难度 | 解法 |
---|---|---|
Addition Chains | ★★☆☆☆ | |
DNA sequence | ★★☆☆☆ | |
Booksort | ★★★☆☆ | |
The Rotation Game | ★★★☆☆ | 迭代加深的公认经典题,理解“最少剩余步数” |
Paint on a Wall | ★★★☆☆ | The Rotation Game 的简单变形 |
Escape from Tetris | ★★★★☆ | |
Maze | ★★★★☆ | |
Rubik 2×2×2 | ★★★★★ | 编码麻烦的魔方题 |
题目链接 | 难度 | 解法 |
---|---|---|
Pushing Boxes | ★☆☆☆☆ | 经典广搜 - 推箱子 |
Jugs | ★☆☆☆☆ | 经典广搜 - 倒水问题 |
Space Station Shielding | ★☆☆☆☆ | FloodFill |
Knight Moves | ★☆☆☆☆ | 棋盘搜索 |
Knight Moves | ★☆☆☆☆ | 棋盘搜索 |
Eight | ★★☆☆☆ | 经典八数码 |
Currency Exchange | ★★☆☆☆ | SPFA |
The Postal Worker Rings | ★★☆☆☆ | SPFA |
ROADS | ★★☆☆☆ | 优先队列应用 |
Ones | ★★☆☆☆ | 同余搜索 |
Dogs | ★★☆☆☆ | |
Lode Runner | ★★☆☆☆ | |
Hike on a Graph | ★★☆☆☆ | |
Find The Multiple | ★★☆☆☆ | 同余搜索 |
Different Digits | ★★★☆☆ | 同余搜索 |
Magic Multiplying Machine | ★★★☆☆ | 同余搜索 |
Remainder | ★★★☆☆ | 同余搜索 |
Escape from Enemy Territory | ★★★☆☆ | 二分答案 + BFS |
Will Indiana Jones Get | ★★★☆☆ | 二分答案 + BFS |
Fast Food | ★★★☆☆ | SPFA |
Invitation Cards | ★★★☆☆ | SPFA |
Galactic Import | ★★★☆☆ | SPFA |
XYZZY | ★★★☆☆ | 最长路判环 |
Intervals | ★★★☆☆ | 差分约束 |
King | ★★★☆☆ | 差分约束 |
Integer Intervals | ★★★☆☆ | 差分约束 |
Sightseeing trip | ★★★☆☆ | 无向图最小环 |
N-Credible Mazes | ★★★☆☆ | 多维空间搜索,散列HASH |
Spreadsheet | ★★★☆☆ | 建立拓扑图后广搜 |
Frogger | ★★★☆☆ | 同余搜索 |
Ministry | ★★★☆☆ | 需要存路径 |
Gap | ★★★☆☆ | A* |
Maze | ★★★☆☆ | 二进制压缩钥匙的状态 |
Hike on a Graph | ★★★☆☆ | |
All Discs Considered | ★★★☆☆ | |
Roads Scholar | ★★★☆☆ | SPFA |
Holedox Moving | ★★★☆☆ | |
昂贵的聘礼 | ★★★☆☆ | |
Maze Stretching | ★★★☆☆ | |
Treasure of the Chimp | ★★★☆☆ | |
Is the Information Reliable | ★★★☆☆ | 最长路判环 |
It’s not a Bug, It’s a | ★★★☆☆ | |
Warcraft | ★★★☆☆ | |
Escape | ★★★☆☆ | |
Bloxorz I | ★★★☆☆ | 当年比较流行这个游戏 |
Up and Down | ★★★★☆ | 离散化 + BFS |
Sightseeing | ★★★★☆ | SPFA |
Cliff Climbing | ★★★★☆ | 日本人的题就是这么长 |
Cheesy Chess | ★★★★☆ | 仔细看题 |
The Treasure | ★★★★☆ | |
Chessman | ★★★★★ | 弄清状态同余的概念 |
Puzzle | ★★★★★ | 几乎尝试了所有的搜索 -_- |
Unblock Me | ★★★★★ | 8进制压缩状态,散列HASH,位运算加速 |
题目链接 | 难度 | 解法 |
---|---|---|
Solitaire | ★★★☆☆ | |
A Game on the Chessboard | ★★★☆☆ | |
魔板 | ★★★★☆ | |
Tobo or not Tobo | ★★★★☆ | |
Eight II | ★★★★★ |