DFS
排列数字
题目描述
给定一个整数 n𝑛,将数字 1∼n1∼𝑛 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入
共一行,包含一个整数 n𝑛。
输出
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10;
int n;
bool st[N];
int path[N];
void dfs(int u)
{
if(u == n)
{
for(int i = 0; i < n; i++)
cout << path[i] << " ";
cout << endl;
return;
}
for(int i = 1; i <= n; i++)
{
if(!st[i])
{
path[u] = i;
st[i] = true;
dfs(u + 1);
path[u] = 0;
st[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(0);
return 0;
}
再介绍一种使用库函数next_premutation的方法
这个函数的作用是会对给定的数组排列,默认按照升序,并且还会去重,当排列到最后一种之后,再排列就会返回false了。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10;
int n;
int a[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) a[i] = i;
do
{
for(int i = 1; i <= n; i++)
cout << a[i] << " ";
cout << endl;
}while(next_permutation(a + 1, a + n + 1));
return 0;
}
n-皇后问题
题目描述
n−𝑛−皇后问题是指将 n𝑛 个皇后放在 n×n𝑛×𝑛 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n𝑛,请你输出所有的满足条件的棋子摆法。
输入
共一行,包含整数 n𝑛。
输出
每个解决方案占 n𝑛 行,每行输出一个长度为 n𝑛 的字符串,用来表示完整的棋盘状态。
其中 .
表示某一个位置的方格状态为空,Q
表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
题解
题目要求每一个皇后的行和列以及对角线和反对角线都没有其他皇后,那么我们每一行只放一个皇后,并且在放该皇后的时候判断列和对角线和反对角线上是否有皇后,如果有那么这种方案一定不合法,可以剪枝掉。
所以说只需要在每一层放一个,那么 dfs 每一层,每一层的搜索逻辑就是从前往后遍历
#include<iostream>
using namespace std;
const int N = 20; // 需要增加对角线数组,判断对角线是否其他皇后存在,对角线的个数为 2n-1 个
char g[N][N];
bool col[N], dg[N], udg[N];
int n;
void dfs(int u)
{
if(u == n)
{
for(int i = 0; i < n; i++)
puts(g[i]);
puts("");
return;
}
for(int i = 0; i < n; i++)
{
if(!col[i] && !dg[u - i + n] && !udg[u + i]) // 这里对对角线的运用是一种映射,下面会有图解
{
g[u][i] = 'Q';
col[i] = dg[u - i + n] = udg[u + i] = true;
dfs(u + 1);
g[u][i] = '.';
col[i] = dg[u - i + n] = udg[u + i] = false;
}
}
}
int main()
{
cin >> n;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
g[i][j] = '.';
dfs(0);
return 0;
}
迷宫
题目描述
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n𝑛∗𝑛 的格点组成,每个格点只有2种状态,.
和#
,前者表示可以通行后者表示不能通行。
同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#),则看成无法办到。
注意:A、B不一定是两个不同的点。
输入
第1行是测试数据的组数 k𝑘,后面跟着 k𝑘 组输入。
每组测试数据的第1行是一个正整数 n𝑛,表示迷宫的规模是 n∗n𝑛∗𝑛 的。
接下来是一个 n∗n𝑛∗𝑛 的矩阵,矩阵中的元素为.
或者#
。
再接下来一行是 4 个整数 ha,la,hb,lbℎ𝑎,𝑙𝑎,ℎ𝑏,𝑙𝑏,描述 A𝐴 处在第 haℎ𝑎 行, 第 la𝑙𝑎 列,B𝐵 处在第 hbℎ𝑏 行, 第 lb𝑙𝑏 列。
注意到 ha,la,hb,lbℎ𝑎,𝑙𝑎,ℎ𝑏,𝑙𝑏 全部是从 0 开始计数的。
输出
k行,每行输出对应一个输入。
能办到则输出“YES”,否则输出“NO”。
数据范围
1≤n≤100
题解
这题可以使用DFS也可以使用BFS,是DFS的连通性模型,问一个矩阵中一个点是否能到另一个点。
DFS代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110;
char g[N][N];
bool st[N][N];
int xa, ya, xb, yb, n;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool dfs(int x, int y)
{
if(g[x][y] == '#') return false;
if(x == xb && y == yb) return true;
st[x][y] = true; // 在图内部搜索不需要恢复现场
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n) continue;
if(st[a][b]) continue; // 一旦找到出口,直接返回
if(dfs(a, b)) return true;
}
return false;
}
int main()
{
int T; cin >> T;
while(T--)
{
cin >> n;
for(int i = 0; i < n; i++) cin >> g[i];
cin >> xa >> ya >> xb >> yb;
memset(st, 0, sizeof st);
if(dfs(xa, ya)) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
BFS代码
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
char g[N][N];
int xa, ya, xb, yb, n;
bool st[N][N];
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
bool bfs(int x, int y)
{
queue<PII> q; // 注意多组测试用例,一定注意每次清空,我在这debug了一个小时
if(g[x][y] == '#') return false;
if(x == xb && y == yb) return true;
st[x][y] = true;
q.push({x, y});
while(q.size())
{
PII t = q.front();
q.pop();
// 当然也可以在出队的时候判断是否到达终点
for(int i = 0; i < 4; i++)
{
int a = t.first + dx[i], b = t.second + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n) continue;
if(st[a][b]) continue;
if(g[a][b] == '#') continue;
if(a == xb && b == yb) return true;
q.push({a, b});
st[a][b] = true;
}
}
return false;
}
int main()
{
int t; cin >> t;
while(t--)
{
cin >> n;
for(int i = 0; i < n; i++) cin >> g[i];
cin >> xa >> ya >> xb >> yb;
memset(st, 0, sizeof st);
if(bfs(xa, ya)) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
红与黑
题目描述
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。
你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入
输入包括多个数据集合。
每个数据集合的第一行是两个整数 W𝑊 和 H𝐻,分别表示 x𝑥 方向和 y𝑦 方向瓷砖的数量。
在接下来的 H𝐻 行中,每行包括 W𝑊 个字符。每个字符表示一块瓷砖的颜色,规则如下
1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
当在一行中读入的是两个零时,表示输入结束。
输出
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
数据范围
1≤W,H≤20
题解
本题同样可以使用BFS和DFS,也是连通性问题
DFS代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 25;
char g[N][N];
int n, m;
bool st[N][N];
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int dfs(int x, int y)
{
int res = 0;
st[x][y] = true;
res ++;
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= m) continue;
if(st[a][b]) continue;
if(g[a][b] == '#') continue;
res += dfs(a, b);
}
return res;
}
int main()
{
while(cin >> m >> n && n != 0)
{
memset(st, 0, sizeof st);
int x = 0, y = 0;
for(int i = 0; i < n; i++)
{
cin >> g[i];
for(int j = 0; j < m; j++)
if(g[i][j] == '@') x = i, y = j;
}
cout << dfs(x, y) << endl;
}
return 0;
}
BFS代码
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 25;
char g[N][N];
int n, m;
bool st[N][N];
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int bfs(int x, int y)
{
int res = 0;
queue<PII> q;
q.push({x, y});
st[x][y] = true;
while(q.size())
{
PII t = q.front(); q.pop();
res++;
for(int i = 0; i < 4; i++)
{
int a = t.first + dx[i], b = t.second + dy[i];
if(a < 0 || a >= n || b < 0 || b >= m) continue;
if(g[a][b] == '#') continue;
if(st[a][b]) continue;
q.push({a, b});
st[a][b] = true;
}
}
return res;
}
int main()
{
while(cin >> m >> n && (n != 0 && m != 0))
{
memset(st, 0, sizeof st);
for(int i = 0; i < n; i++) cin >> g[i];
int x = 0, y = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(g[i][j] == '@') x = i, y = j;
cout << bfs(x, y) << endl;
}
return 0;
}
单词接龙
题目描述
单词接龙是一个与我们经常玩的成语接龙相类似的游戏。
现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多被使用两次。
在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish ,如果接成一条龙则变为 beastonish。
我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度,例如 at 和 atide 间不能相连。
输入
输入的第一行为一个单独的整数 n𝑛 表示单词数,以下 n𝑛 行每行有一个单词(只含有大写或小写字母,长度不超过20),输入的最后一行为一个单个字符,表示“龙”开头的字母。
你可以假定以此字母开头的“龙”一定存在。
输出
只需输出以此字母开头的最长的“龙”的长度。
数据范围
n≤20𝑛≤20,
单词随机生成。
题解
对于两个单词拼接成一个最长的单词,需要重合最短的长度,这样才能保证拼接之后的单词最长。
可以先预处理,将每两个可以拼接的单词保存下来,并记录这两个单词的最短重合部分。
在dfs的时候还需要一个数组记录单词使用的次数,题目要求不能超过2次。
每一种拼接顺序都是一个方案,需要恢复现场
#include<iostream>
#include<string>
using namespace std;
const int N = 21;
string word[N];
int g[N][N];
int n;
int res;
int used[N];
void dfs(string str, int last)
{
res = max(res, (int) str.size());
used[last]++;
for(int i = 0; i < n; i++)
{
if(g[last][i] && used[i] < 2)
dfs(str + word[i].substr(g[last][i]), i);
}
used[last]--;
}
int main()
{
cin >> n;
for(int i = 0; i < n; i++) cin >> word[i];
char start; cin >> start;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
{
string f = word[i], s = word[j];
for(int k = 1; k < min(f.size(), s.size()); k++)
{
if(f.substr(f.size() - k, k) == s.substr(0, k))
{
g[i][j] = k;
break;
}
}
}
for(int i = 0; i < n; i++)
{
if(word[i][0] == start)
dfs(word[i], i);
}
cout << res << endl;
return 0;
}
DFS的剪枝与优化
- 优化搜索顺序(优先搜索分支较少的点)
- 排除等效冗余
- 可行性剪枝
- 最优性剪枝
首先考虑一种搜索顺序,保证每一种情况可以搜索到,这样才能保证答案的正确性
然后根据题目性质去优化
小猫爬山
翰翰和达达饲养了 N𝑁 只小猫,这天,小猫们要去爬山。
经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
翰翰和达达只好花钱让它们坐索道下山。
索道上的缆车最大承重量为 W𝑊,而 N𝑁 只小猫的重量分别是 C1、C2……CN𝐶1、𝐶2……𝐶𝑁。
当然,每辆缆车上的小猫的重量之和不能超过 W𝑊。
每租用一辆缆车,翰翰和达达就要付 11 美元,所以他们想知道,最少需要付多少美元才能把这 N𝑁 只小猫都运送下山?
输入格式
第 11 行:包含两个用空格隔开的整数,N𝑁 和 W𝑊。
第 2..N+12..𝑁+1 行:每行一个整数,其中第 i+1𝑖+1 行的整数表示第 i𝑖 只小猫的重量 Ci𝐶𝑖。
输出格式
输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。
数据范围
1≤N≤181≤𝑁≤18,
1≤Ci≤W≤108
题解
每一次做决策,将这个小猫分到之前的组中,还是开一个新的组。可以按照这个顺序搜素
剪枝:
1、优化搜索顺序:搜索顺序:先分配重量大的猫,这样这只猫就能占一个组的最大重量,这样对于其他的猫分配到这个组中的概率就会小很多,这样搜索到的分支就会少一点
2、可行性剪枝:对于每一组,都需要判断当前待分配的猫是否会超重,若超重就不能放入当前组中
3、最优性剪枝:答案需要为最小的组数,当搜索的组大于当前以及收集的答案,那么需要结束当前搜索。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 20;
int n, m;
int w[N];
int res, sum;
int s[N];
void dfs(int u, int k) // u:当前分配多少辆车;k:当前待分配的猫
{
if(u >= res) return; // 最优性剪枝
if(k == n)
{
res = u;
return;
}
for(int i = 0; i < u; i++)
{
if(s[i] + w[k] <= m) // 可行性剪枝
{
s[i] += w[k];
dfs(u, k + 1);
s[i] -= w[k]; // 恢复现场
}
}
s[u] += w[k];
dfs(u + 1, k + 1);
s[u] -= w[k]; // 恢复现场
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++) cin >> w[i];
sort(w, w + n, greater<int>()); // 优化搜索顺序
res = N;
dfs(0, 0);
cout << res << endl;
return 0;
}
数独
数独 是一种传统益智游戏,你需要把一个 9×99×9 的数独补充完整,使得数独中每行、每列、每个 3×33×3 的九宫格内数字 1∼91∼9 均恰好出现一次。
请编写一个程序填写数独。
输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 8181 个字符,代表数独的 8181 个格内数据(顺序总体由上到下,同行由左到右)。
每个字符都是一个数字(1−91−9)或一个 .
(表示尚未填充)。
您可以假设输入中的每个谜题都只有一个解决方案。
文件结尾处为包含单词 end
的单行,表示输入结束。
输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。
题解
搜索顺序:在网格中任意选择一个空格子,然后填它可以填的数
结束条件:当网格被填满,或者这一次搜索到了结果
剪枝:
1、优化搜索顺序:每一次选择的空格,优先选择可填数字少的空格,这样该分支的数量会少。比如填了数字少的空格,会影响到其他空格可以填的数字,并且只会减少其他空格方案数。
2、可行性剪枝:当前空格选择的方案不能与行、列、方格重复
对于当前空格的可填数字,使用位运算优化。
规定:对于一个数字的二进制来说,当前位为1代表这个位可以填,为0表示以及填过了。
题目要求的是九宫格的数独,所以是一个9x9的网格;所以可以使用三个数组,分别表示行、列、小方格。这三个数组中存储的是一个9位的二进制数,表示当前行、列、小方格可以填哪些数字。
那么对于一个空格可以填哪些数字,只需要将三个数组的当前位置&起来。比如当前空格的位置位(x,y),那么当前位置可以的状态(这个状态表示,当前位置所有可以填的数字的集合,是一个二进制数,每一位的0或1表示该位对应的十进制数可填或者不可填)row[x] & col[y] & cell[x/3][y/3]
lowbit函数:返回当前数字的最低位1,对应的十进制数
预处理:对三个数组预处理为整个网格都为空的状态,对应的就是二进制上每一位都为1
还需要预处理两个数组,作用为在O(1)下找到每一个十进制下有多少位二进制1;另一个的作用是快速找到二进制的1在第几位,这就是需要填的数。log (2^n) = n
#include<iostream>
using namespace std;
const int N = 9, M = 1 << N;
char str[100];
int row[N], col[N], cell[3][3];
int ones[M]; // ones[i]:i的二进制下1的个数
int map[M]; // map[i]:找到 i 对应的二进制位,首先这个 i 一定是2的幂,这样的作用是找到可以填的数
void init()
{
for(int i = 0; i < N; i++) row[i] = col[i] = (1 << N) - 1;
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
cell[i][j] = (1 << N) - 1;
}
void draw(int x, int y, int t, bool is_set)
{
if(is_set) str[x * N + y] = t + '1';
else str[x * N + y] = '.';
int v = 1 << t;
if(!is_set) v = -v;
row[x] -= v;
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
int get(int x, int y)
{
return row[x] & col[y] & cell[x / 3][y / 3];
}
int lowbit(int x)
{
return x & -x;
}
bool dfs(int cnt)
{
if(!cnt) return true;
// 优化搜索顺序
int x, y;
int minv = 10;
for(int i = 0; i < N; i++)
{
for(int j = 0; j < N; j++)
{
if(str[i * N + j] == '.')
{
int state = get(i, j);
if(minv > ones[state])
{
minv = ones[state];
x = i, y = j;
}
}
}
}
// 填数
int state = get(x, y);
for(int i = state; i > 0; i -= lowbit(i))
{
int t = map[lowbit(i)];
draw(x, y, t, true);
if(dfs(cnt - 1)) return true;
draw(x, y, t, false); // 恢复现场
}
return false;
}
int main()
{
for(int i = 0; i < 1 << N; i++)
for(int j = 0; j < N; j++)
ones[i] += i >> j & 1;
for(int i = 0; i < N; i++) map[1 << i] = i;
while(cin >> str && str[0] != 'e')
{
init();
int cnt = 0;
for(int i = 0; i < N; i++)
for(int j = 0; j < N; j++)
if(str[i * N + j] != '.')
{
int t = str[i * N + j] - '1';
draw(i, j, t, true);
}
else cnt++;
dfs(cnt);
puts(str);
}
return 0;
}
木棒
乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 5050 个长度单位。
然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。
每一节木棍的长度都用大于零的整数表示。
输入格式
输入包含多组数据,每组数据包括两行。
第一行是一个不超过 6464 的整数,表示砍断之后共有多少节木棍。
第二行是截断以后,所得到的各节木棍的长度。
在最后一组数据之后,是一个零。
输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
数据范围
数据保证每一节木棍的长度均不大于 50。
题解
规定:由 木棍 拼接而成为 木棒
首先确定搜索顺序:题目要求计算出最小的木棒长度,所以可以从小到大枚举木棒的长度。
确定木棒的长度之后,搜索整个木棍数组,尝试组成该长度的木棒。
剪枝:
1、优化搜索顺序:
- 目标木棒长度应可整除木棍长度之和。
- 将木棍降序排序。先搜索长度更大的木棍,可以优先达成目标木棒长度,减少搜索分支。
2、排除等效冗余:
- 按组合数枚举组成每一根木棒的木棍。每一根木棒的长度是有木棍组成的,无关乎木棍的长度顺序。
- 当前木棍组成当前木棒失败了,应忽略后面和当前木棍相等长度的木棍。
- 组成木棒的第一根木棍失败了,则直接失败。
- 组成木棒的最后一根木棒失败了,则直接失败。
利用反证法对最后两点进行证明:
1、设a木棍组成一根木棒(设该木棒为A)的第一根失败了,那么由a木棍后面的木棍b作为第一根木棍,并且可以成功组成目标长度的木棒。那么在A木棒之后一定存在一根木棒B是由a木棍组成的;而组成B木棒的木棍可以任意排列,也就是说a木棍可以成为B木棒的第一根木棍;而A木棒和B木棒等等全部的被组成的木棒不存在先后顺序,可以任意交换,也是最优解;也就是说A木棒和B木棒可以交换,那么这个时候a木棍又可以作为A木棒的第一根组成的木棍了;这和前提矛盾,所以说:组成木棒的第一根木棍失败了,则直接失败。
2、设a木棍组成A木棒的最后一根木棍失败了,并且可以成功组成目标长度的木棒(这一点很重要,在代码中会有体现);那么A木棒的最后一根木棍一定是由a木棍的后面的多根木棍组成的,因为后面的木棍一定长度比a木棍小,不妨设为木棍b和木棍c;那么a木棍可以组成后面的木棒,则a木棍可以和b、c木棍交换;那么和前提矛盾。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 65;
int w[N], n;
int sum, len;
bool st[N];
bool dfs(int u, int s, int start) // u:当前组成的木棒个数;s:当前木棒的长度;start:组合数搜索
{
if(u * len == sum) return true;
if(s == len) return dfs(u + 1, 0, 0); // 当前已经组成了目标长度的木棒,新开一根木棒
for(int i = start; i < n; i++)
{
if(st[i]) continue; //
if(s + w[i] > len) continue; //
st[i] = true;
if(dfs(u, s + w[i], i + 1)) return true; // 链接可行解
st[i] = false; // 恢复现场
// 排除等效冗余
// 当程序运行到这里,则当前木棍组成木棒失败了
if(!s) return false; // 组成第一根失败
if(s + w[i] == len) return false; // 组成最后一根失败
// 当前木棍组成失败,略过后面相等的木棍
int j = i;
while(j < n && w[i] == w[j]) j++;
i = j - 1;
}
return false;
}
int main()
{
while(cin >> n && n)
{
memset(st, 0, sizeof st);
sum = 0;
for(int i = 0; i < n; i++)
{
cin >> w[i];
sum += w[i];
}
sort(w, w + n, greater<int>()); // 优化搜索顺序
len = 1; // 从小到大固定答案
while(1)
{
if(sum % len == 0 && dfs(0, 0, 0)) // 排除等效性冗余
{
cout << len << endl;
break; // 最优解为最小长度
}
len++;
}
}
return 0;
}