文章目录
- 说明
- 习题
- 习7-1 UVA 208 消防车
- 习7-2 UVA 225 黄金图形
- 习7-3 UVA 211 多米诺效应
- 习7-4 UVA 818 切断圆环链(未尝试)
- 习7-5 UVA 690 流水线调度(未尝试)
- 习7-6 UVA 12113 重叠的正方形(未通过,WA)
- 习7-7 UVA 12558 埃及分数(未尝试)
- 习7-8 UVA 12107 数字谜
- 习7-9 UVA 1604 立体八数码问题
- 习7-10 UVA 11214 守卫棋盘
- 习7-11 UVA 12569 树上的机器人规划-简单版
- 习7-12 UVA 1533 移动小球
- 习7-13 UVA 817 数字表达式*(未通过,RE)
- 习7-14 UVA 307 小木棍
- 习7-15 UVA 11882 最大的数
- 习7-16 UVA 11846 找座位(未通过,TLE)
- 习7-17 UVA 11694 Gokigen Naname 谜题
- 习7-18 UVA 10384 推门游戏(未尝试)
说明
本文是我对第七章18道习题的练习总结,建议配合紫书——《算法竞赛入门经典(第2版)》阅读本文。
另外为了方便做题,我在VOJ上开了一个contest,欢迎一起在上面做:第七章习题contest
如果想直接看某道题,请点开目录后点开相应的题目!!!
习题
习7-1 UVA 208 消防车
题意
输入一个n(n≤20)个结点的无向图以及某个节点k,按照字典序从小到大顺序输出从节点1到节点k的所有路径,要求结点不能重复经过。
思路
这个题要事先判断节点1是否可以到达节点k,否则会超时。有很多种方法可以判断:比如DFS遍历,或者用并查集等。
然后DFS遍历即可,但考虑到算法效率,可以采取回溯+剪枝的方案(当然不剪枝也是可以AC的,时间长一点而已)。
我这个题卡在判断节点1是否可以到达节点k这一步上很久。我的代码主函数中第6行原来是:
n = 0;
后来查了很久,才发现改成
n = k;
就能AC。
按照我原来的算法逻辑,n=0的情况下,所给数据有可能出现这一种情况:
如果给出的所有路径中出现的节点都小于k,这样得到的n将小于k。
而这时候节点1肯定无法到达节点k,第一步的判断应该可以给出正确答案。
但结果就是我不改的话会WA,改了就AC。我目前从算法逻辑上仍然没有想明白。
如果有哪位大神知道,请不吝指点。
另外本题可以剪枝,可以在时间复杂度上有重大优化,请参考其他博客。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 21;
int n, k;
vector<int> neigh[N];
int v[N];
int path_count;
vector<int> path;
bool dfs(int u)
{
if (u == k) return true;
for (int i = 0; i < neigh[u].size(); i++) {
if (!v[neigh[u][i]]) {
int x = neigh[u][i];
v[x] = 1;
if (dfs(x)) return true;
}
}
return false;
}
void find_path()
{
path_count++;
for (int i = 0; i < path.size(); i++)
printf("%d%c", path[i], i == path.size()-1 ? '\n' : ' ');
}
void search(int u)
{
if (u == k) { find_path(); return; }
for (int i = 0; i < neigh[u].size(); i++) {
if (!v[neigh[u][i]]) {
int x = neigh[u][i];
v[x] = 1;
path.push_back(x);
search(x);
path.resize(path.size()-1);
v[x] = 0;
}
}
}
int main()
{
int kase = 0;
while (scanf("%d", &k) != EOF) {
int a, b;
int G[N][N];
memset(G, 0, sizeof(G));
n = k;
while (scanf("%d%d", &a, &b), a || b) {
n = max(n, max(a, b));
G[a][b] = G[b][a] = 1;
}
for (int i = 1; i <= n; i++) {
neigh[i].clear();
for (int j = 1; j <= n; j++) {
if (G[i][j]) neigh[i].push_back(j);
}
}
printf("CASE %d:\n", ++kase);
memset(v, 0, sizeof(v));
v[1] = 1;
path_count = 0;
if (dfs(1)) {
path.clear();
memset(v, 0, sizeof(v));
v[1] = 1;
path.push_back(1);
search(1);
}
printf("There are %d routes from the firestation to streetcorner %d.\n", path_count, k);
}
return 0;
}
习7-2 UVA 225 黄金图形
题意
平面上有k个障碍点。从(0,0)点出发,第一次走1个单位,第二次走2个单位,……,第n次走n个单位,恰好回到(0,0)。要求只能沿着东南西北方向走,且每次必须转弯90°(不能沿着同一个方向继续走,也不能后退)。走出的图形可以自交,但不能经过障碍点。
思路
首先这个题目的翻译是有问题的,漏掉了一个很重要的判断条件:每一个落脚点不能与前一个相同(出发时的原点不算)。
然后我就在不知情的情况下各种WA。后来参考了其它博客才通过的。
另外这个题我觉得条件约束给的不好,应该说清楚每个城市的坐标范围,给一个基本约束,比如说坐标范围在-100到100之间。我后来是参考其它博客定义的坐标范围。
不过,没有给坐标范围的话这个题也能做,用两个set分别存储故障点和落脚点,用于判重即可。我开始用了set,因为题意不清的原因提交后WA给改了,就成了现在的代码。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
using namespace std;
const char tow[] = "ensw";
const int dir[4][2] = {{1, 0}, {0, 1}, {0, -1}, {-1, 0}};
typedef pair<int, int> P;
int n, k;
int G[250][250];
const int OFF = 105;
set<P> block;
int path_count;
vector<int> path;
void found_path()
{
for (int i = 0; i < path.size(); i++)
printf("%c", tow[path[i]]);
printf("\n");
path_count++;
}
void dfs(P p)
{
if (path.size() == n) {
if (p == P(0, 0))
found_path();
return;
}
int m = path.size();
for (int i = 0; i < 4; i++) {
if (m && (path[m-1]+1)%4/2 == (i+1)%4/2) continue;
P p1 = p;
bool flag = true;
for (int j = 1; j <= m+1; j++) {
p1.first += dir[i][0];
p1.second += dir[i][1];
int x = p1.first, y = p1.second;
if (abs(x) > OFF || abs(y) > OFF || G[p1.first+OFF][p1.second+OFF] == -1) {flag = false; break;}
}
if (flag && G[p1.first+OFF][p1.second+OFF] != 1) {
path.push_back(i);
G[p1.first+OFF][p1.second+OFF] = 1;
dfs(p1);
G[p1.first+OFF][p1.second+OFF] = 0;
path.resize(m);
}
}
}
int main()
{
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
scanf("%d%d", &n, &k);
int x, y;
memset(G, 0, sizeof(G));
for (int i = 0; i < k; i++) {
scanf("%d%d", &x, &y);
G[x+OFF][y+OFF] = -1;
}
path_count = 0;
path.clear();
dfs(P(0, 0));
printf("Found %d golygon(s).\n\n", path_count);
}
return 0;
}
习7-3 UVA 211 多米诺效应
题意
大概题意是有题目中的28种12的色块,拼成了一个78的矩形,让你算出有多少种色块可以拼出这种矩形并输出编号。
思路
直接dfs即可,对剪枝没有什么要求。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
const int MAXN = 5001;
const int MAXM = 51;
int n, m, ans;
int G[8][9], V[8][9], T[8][9]; //G为原图,V为目标图,T为转换规则表
int C[29]; //编号是否使用过
int d[2][2] = { { 0, 1 }, { 1, 0 } };
void print(int A[8][9]) {
FOR1(i, 0, 6) {
FOR1(j, 0, 7) {
printf("%4d", A[i][j]);
}
puts("");
}
puts("");
}
void DFS(int x, int y, int c) {
if (c == 28) { ans++; print(V); return; }
if (y == 8) x++, y = 0;
if (V[x][y]) DFS(x, y + 1, c); //从左到右,从上到下遍历
else {
for (int i = 0; i < 2; i++) {
int p = x + d[i][0], q = y + d[i][1];
if (p >= 7 || q >= 8 || V[p][q]) continue;
int k = T[G[x][y]][G[p][q]]; //找到对应编号
if (C[k]) continue;
V[x][y] = V[p][q] = k; C[k] = 1;
DFS(x, y + 1, c + 1);
V[x][y] = V[p][q] = 0; C[k] = 0;
}
}
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int kase = 0;
while (scanf("%d", &G[0][0]) == 1) {
FOR1(i, 0, 6) FOR1(j, 0, 7) {
if (i || j) scanf("%d", &G[i][j]);
}
memset(V, 0, sizeof(V));
memset(C, 0, sizeof(C));
int a = 1;
FOR1(i, 0, 6) FOR1(j, i, 6) T[i][j] = T[j][i] = a++; //计算转换表
ans = 0;
if (kase) cout << "\n\n\n";
printf("Layout #%d:\n\n", ++kase);
print(G);
printf("Maps resulting from layout #%d are:\n\n", kase);
DFS(0, 0, 0);
printf("There are %d solution(s) for layout #%d.\n", ans, kase);
}
return 0;
}
习7-4 UVA 818 切断圆环链(未尝试)
题意
思路
代码
习7-5 UVA 690 流水线调度(未尝试)
题意
思路
代码
习7-6 UVA 12113 重叠的正方形(未通过,WA)
题意
给定一个44的棋盘和棋盘上所呈现出来的纸张边缘,如图7-29所示,问用不超过6张22的纸能否摆出这样的形状。
思路
需要剪枝,不然会超时。主要是判断边缘情况,可以直接排除一些正方形,然后直接遍历求解即可。我写的是循环,实际上用DFS可读性更好。
注意我这份代码是WA的,但是跑了基础测试用例以及udebug上的测试用例都能通过。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
char G[12][12];
bool legal[9];
int S[2][6][6], D[2][6][6];
void process_legal() {
FOR1(k, 0, 8) {
legal[k] = true;
if (k == 4) continue; //最中间的无法确定
int i = k / 3, j = k % 3;
if (i == 0 && (!D[1][0][j] || !D[1][0][j + 1])) { legal[k] = false; continue; }
if (i == 2 && (!D[1][4][j] || !D[1][4][j + 1])) { legal[k] = false; continue; }
if (j == 0 && (!D[0][i][0] || !D[0][i + 1][0])) { legal[k] = false; continue; }
if (j == 2 && (!D[0][i][4] || !D[0][i + 1][4])) { legal[k] = false; continue; }
}
}
void put(int k, int A[2][6][6]) {
int i = k / 3, j = k % 3;
A[1][i][j] = A[1][i][j + 1] = A[1][i + 2][j] = A[1][i + 2][j + 1] = 1;
A[1][i + 1][j] = A[1][i + 1][j + 1] = 0;
A[0][i][j] = A[0][i + 1][j] = A[0][i][j + 2] = A[0][i + 1][j + 2] = 1;
A[0][i][j + 1] = A[0][i + 1][j + 1] = 0;
}
bool issame(int A1[2][6][6], int A2[2][6][6]) {
FOR1(i, 0, 1) {
FOR1(j, 0, 4) {
if (i == 0 && j == 4) continue;
FOR1(k, 0, 4) {
if (i == 1 && k == 4) continue;
if (A1[i][j][k] != A2[i][j][k]) return false;
}
}
}
return true;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int T = 0;
while (fgets(G[0], 20, stdin) && G[0][0] != '0') {
FOR1(i, 0, 4) {
if (i) fgets(G[i], 20, stdin);
FOR1(j, 0, 8) {
D[j & 1][i - ((j + 1) & 1)][j / 2] = (G[i][j] == ' ') ? 0 : 1;
}
}
process_legal();
int find = false;
FOR1(i1, 0, 8) {
memset(S, 0, sizeof(S));
if (find) break;
if (!legal[i1]) continue;
put(i1, S);
if (issame(S, D)) {
find = true; break;
}
FOR1(i2, 0, 8) {
if (find) break;
if (!legal[i2] || i1 == i2) continue;
put(i2, S);
if (issame(S, D)) {
find = true; break;
}
FOR1(i3, 0, 8) {
if (find) break;
if (!legal[i3] || i1 == i3 || i2 == i3) continue;
put(i3, S);
if (issame(S, D)) {
find = true; break;
}
FOR1(i4, 0, 8) {
if (find) break;
if (!legal[i4] || i1 == i4 || i2 == i4 || i3 == i4) continue;
put(i4, S);
if (issame(S, D)) {
find = true; break;
}
FOR1(i5, 0, 8) {
if (find) break;
if (!legal[i5] || i1 == i5 || i2 == i5 || i3 == i5 || i4 == i5) continue;
put(i5, S);
if (issame(S, D)) {
find = true; break;
}
FOR1(i6, 0, 8) {
if (find) break;
if (!legal[i6] || i1 == i6 || i2 == i6 || i3 == i6 || i4 == i6 || i5 == i6) continue;
put(i6, S);
if (issame(S, D)) {
find = true; break;
}
}
}
}
}
}
}
if (find) printf("Case %d: Yes\n", ++T);
else printf("Case %d: No\n", ++T);
}
return 0;
}
习7-7 UVA 12558 埃及分数(未尝试)
题意
思路
代码
习7-8 UVA 12107 数字谜
题意
给出一个数字谜,要求修改尽量少的数,使修改后的数字谜只有唯一解。注意不能有前导零,输出字典序最小的答案。
思路
这个题目需要两次DFS,第一次是找出表达式,第二次是判断表达式的解是否唯一。
第一次DFS可以做的剪枝不是特别明显,我只加了尾数相乘得到的尾数确定这个剪枝。当然还可以做别的剪枝,代码上要稍微麻烦些。
第二次DFS有明显的的2个剪枝:一是只需要枚举数字a和b,c就能算出来,然后验证c的正确性即可;二是只要发现多余1个解,立即返回false。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
char S0[3][5], S[3][5], S2[3][5];
int maxd, L[3];
int X[3];
int check2() {
int a = atoi(S2[0]), b = atoi(S2[1]);
int c = a * b;
int T[5];
FOR2(i, L[2] - 1, 0) {
T[i] = c % 10;
c /= 10;
if (S[2][i] != '*' && S[2][i] != T[i] + 48)
return 0;
}
if (c) return 0;
if (T[0] == 0 && L[2] > 1) return 0;
return 1;
}
int DFS2(int m, int n) {
if (m == 2) //原来这里是m==3,后来看别人代码才知道这里提前判断可以大大降低复杂度
return check2();
if (S2[m][n] != '*'){
if (n == L[m] - 1) return DFS2(m + 1, 0);
return DFS2(m, n + 1);
}
int res = 0;
FOR1(i, 0, 9) {
if (L[m] > 1 && n == 0 && i == 0) continue; //前导0的情况不考虑
S2[m][n] = i + 48;
if (n == L[m] - 1) res += DFS2(m + 1, 0);
else res += DFS2(m, n + 1);
S2[m][n] = S[m][n];
if (res > 1) break; //加剪枝
}
return res;
}
bool DFS(int m, int n, int d) { //当前搜索到第m个数的第n位,深度为d
if (m == 3 || d == maxd) {
if (d != maxd) return false; //因为更低深度的已经搜索过
//由于TLE,以下加一些剪枝(似乎效果一般)
int a = S[0][L[0] - 1] - 48, b = S[1][L[1] - 1] - 48, c = S[2][L[2] - 1] - 48;
if (a != '*' - 48 && b != '*' - 48 && c != '*' - 48) { //加剪枝
if (a * b % 10 != c) return false;
}
if (a != '*' - 48 && c != '*' - 48) { //加剪枝
if (a % 2 == 0 && c % 2 == 1) return false;
}
if (b != '*' - 48 && c != '*' - 48) { //加剪枝
if (b % 2 == 0 && c % 2 == 1) return false;
}
//以上是新加的剪枝
memcpy(S2, S, sizeof(S));
if (DFS2(0, 0) == 1) {
printf("%s %s %s\n", S[0], S[1], S[2]);
return true;
}
return false;
}
FOR1(i, 0, 10) {
char c = (i == 0) ? '*' : i + 47;
if (L[m] > 1 && n == 0 && c == '0') continue; //前导0的情况不考虑
int d1 = d + (S0[m][n] != c);
//if (d1 > maxd) continue; //剪枝
S[m][n] = c;
if (n == L[m] - 1 && DFS(m + 1, 0, d1)) return true;
if (n != L[m] - 1 && DFS(m, n + 1, d1)) return true;
S[m][n] = S0[m][n];
}
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int cas = 0;
while (scanf("%s%s%s", S0[0], S0[1], S0[2]) == 3) {
FOR1(i, 0, 2) L[i] = strlen(S0[i]);
printf("Case %d: ", ++cas);
FOR1(i, 0, 10) {
maxd = i;
memcpy(S, S0, sizeof(S0));
if (DFS(0, 0, 0)) break;
}
memset(S0, 0, sizeof(S0));
}
return 0;
}
习7-9 UVA 1604 立体八数码问题
题意
有8个立方体,按照相同方式着色(如图7-31(a)所示,相对的面总是着相同颜色),然后以相同的朝向摆成一个3*3的方阵,空出一个位置(如图7-31(b)所示,空位由输入决定)。
每次可以把一个立方体“滚动”一格进入空位,使它原来的位置成为空位,如图7-32所示。
你的任务是用最少的移动使得上表面呈现出指定的图案。输入空位的坐标和目标状态中上表面各个位置的颜色,输出最小移动步数。
思路
这个题主要有3点需要注意:
1、整体思路,显然可以用BFS,但是直接用BFS会超时。我这里用了双向BFS,实际上也可以用BFS+优先队列等其他方法。
2、可以控制两个方向的搜索深度,时间效率可进一步优化。本题的双向搜索深度分别为20和10,时间效率还是比较优的。
3、编码方式,每个立方体有6种状态,加上空位状态总共有7种,所以整个状态是7^9,可以用编解码表示,详见代码。
1、还可以用BFS+优先队列,不仅效率更高,写法也更简单。
2、理论上应该某个方向的某一深度全部搜索完毕之后,再搜索另一方向,否则可能出现错误。但我看有的程序并不是这么做的,保留疑问。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
const int MAXS = 40353607 + 10; //总共有7^9状态数
int x, y;
int T[9];
queue<int> Q[2];
int ST[2][MAXS]; //对每个状态,0表示上蓝前白,1为上蓝前红,2为上白前蓝,3为上白前红,4为上红前蓝,5为上红前白,6为空位
int d[4][2] = { { -1, 0 }, { 1, 0 }, { 0, 1 }, { 0, -1 } };
int RT[2][6] = { { 2, 4, 0, 5, 1, 3 }, { 5, 3, 4, 1, 2, 0 } }; //事先计算好旋转的状态,与d相对应
int BWR(char c) {
if (c == 'B') return 0;
if (c == 'W') return 1;
if (c == 'R') return 2;
return 3;
}
int encode(vector<int> vt) {
int res = 0;
FOR1(i, 0, 8)
res = res * 7 + vt[i];
return res;
}
vector<int> decode(int mm) {
vector<int> vt;
FOR1(i, 0, 8) vt.push_back(0);
FOR2(i, 8, 0) {
vt[i] = mm % 7;
mm /= 7;
}
return vt;
}
void DFS_PUSH(int k, int s) {
if (k == 9) {
Q[1].push(s); ST[1][s] = 1;
return;
}
FOR1(i, T[k] * 2, min(6, T[k] * 2 + 1))
DFS_PUSH(k + 1, s * 7 + i);
}
int BFS() {
memset(ST, 0, sizeof(ST));
vector<int> vt, vt0;
FOR1(i, 0, 8) vt.push_back(3);
vt[y * 3 + x] = 6; //注意x和y是反过来的
int s = encode(vt);
Q[0].push(s); ST[0][s] = 1; //初始状态放入正向队列
DFS_PUSH(0, 0); //把所有可能目标状态放入反向队列
int depth[2]; depth[0] = depth[1] = 1;
while (!Q[0].empty() && !Q[1].empty()) { //把双向BFS放在一段代码里面容易出细节错误
int r = (depth[0] > 21) ? 1 : 0;
s = Q[r].front();
depth[r] = max(depth[r], ST[r][s]);
if (depth[r] + depth[1 - r] - 1 > 30) return -1;
Q[r].pop();
vt0 = decode(s);
if (ST[1 - r][s]) return ST[r][s] + ST[1 - r][s] - 2;
FOR1(i, 0, 8) {
if (vt0[i] != 6) continue;
int a0 = i / 3, b0 = i % 3;
FOR1(j, 0, 3) {
int a = a0 - d[j][0], b = b0 - d[j][1];
if (a >= 0 && a <= 2 && b >= 0 && b <= 2) {
vt = vt0;
int ni = a * 3 + b;
vt[i] = RT[j / 2][vt[ni]];
vt[ni] = 6;
int s1 = encode(vt);
if (!ST[r][s1]) {
Q[r].push(s1);
ST[r][s1] = ST[r][s] + 1;
if (ST[1 - r][s1]) return ST[r][s1] + ST[1 - r][s1] - 2;
}
}
}
}
}
return -1;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
while (cin >> x >> y && x) {
x--, y--;
char c[10];
FOR1(i, 0, 8) {
scanf("%s", c);
T[i] = BWR(c[0]);
}
printf("%d\n", BFS());
FOR1(r, 0, 1) while (!Q[r].empty()) Q[r].pop();
}
return 0;
}
习7-10 UVA 11214 守卫棋盘
题意
输入一个n*m棋盘(n,m<10),某些格子有标记。用最少的皇后守卫(即占据或者攻击)所有带标记的格子。
思路
类似于经典N皇后问题,这个题是迭代加深搜索。这篇博客说的比较清楚:
https://blog.csdn.net/qq_40772692/article/details/80914092
我在做的过程中:
1、之前犯了一个审题理解错误:皇后保护的不仅有斜线方向,还有行和列方向,我以为只有斜线方向。
2、有两种记录状态的方式,我这里的方式跟网上主流的不太一样,各有优缺点。我的方式需要保存整个数组,不过好处是遍历的状态少,网上主流的则相反。
3、写的时候还是犯了很多细节错误,逐渐调试得到最终正确结果。这说明我的功力还是差很多。
4、如果没加这个前4次搜索没成功则直接输出5的剪枝,可能这个题目还是会TLE。这说明我的方法复杂度可能还是要大于网上主流的方法。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
int n, m, maxd;
char S[11][11];
int T, V[4][20]; //两个方向上数组包含的棋子数量,本来想用位运算,后来发现不行(其实也行,只不过搜索的时候要全搜)
int tow[8][2] = { { 1, -1 }, { -1, 1 }, { 1, 1 }, { -1, -1 }, { 0, -1 }, { -1, 0 }, { 1, 0 }, { 0, 1 } };
bool DFS(int x, int y, int d) { //当前搜索到x,y位置,深度为d
if (x == n || d == maxd) {
return !T; //T表示总的计数
}
int a = x + y, b = y + n - 1 - x;
while (x < n && V[0][a] == 0 && V[1][b] == 0 && V[2][x] == 0 && V[3][y] == 0) {
y++;
if (y == n) x++, y = 0;
a = x + y, b = y + n - 1 - x;
}
if (x == n)
return !T;
int nx = x, ny = y + 1;
if (ny == n) nx++, ny = 0;
if (DFS(nx, ny, d)) return true; //这是不在当前位置放棋子的情况
char S2[11][11]; //这三个备份值应定义为局部变量
int T2, V2[4][20];
memcpy(V2, V, sizeof(V)); //保存
memcpy(S2, S, sizeof(S)); //保存
T2 = T;
if (S[x][y] == 'X') { //注意当前位置可能有也可能没有棋子
V[0][a]--, V[1][b]--, V[2][x]--, V[3][y]--, T -= 4;
S[x][y] = '.';
}
FOR1(i, 0, 7) {
int x1 = x + tow[i][0], y1 = y + tow[i][1];
while (0 <= x1 && x1 <= n - 1 && 0 <= y1 && y1 <= m - 1) {
if (S[x1][y1] == 'X') {
V[0][x1 + y1]--, V[1][y1 + n - 1 - x1]--, V[2][x1]--, V[3][y1]--, T -= 4;
S[x1][y1] = '.';
}
x1 += tow[i][0], y1 += tow[i][1];
}
}
if (DFS(nx, ny, d + 1)) return true;
memcpy(V, V2, sizeof(V2)); //还原
memcpy(S, S2, sizeof(S2)); //还原
T = T2;
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int cas = 0;
while (cin >> n && n) {
cin >> m;
T = 0;
memset(V, 0, sizeof(V));
FOR1(i, 0, n - 1) {
scanf("%s", S[i]);
FOR1(j, 0, m - 1) {
if (S[i][j] == 'X') {
V[0][i + j]++;
V[1][j + n - 1 - i]++;
V[2][i]++;
V[3][j]++;
T += 4;
}
}
}
printf("Case %d: ", ++cas);
FOR1(i, 0, 5) {
if (i == 5) {
printf("5\n"); //剪枝1
break;
}
maxd = i;
if (DFS(0, 0, 0)) {
printf("%d\n", maxd);
break;
}
}
}
return 0;
}
习7-11 UVA 12569 树上的机器人规划-简单版
题意
有一棵n(4≤n≤15)个结点的树,其中一个结点有一个机器人,还有一些结点有石头。每步可以把一个机器人或者石头移到一个相邻结点。任何情况下一个结点里不能有两个东西(石头或者机器人)。输入每个石头的位置和机器人的起点和终点,求最小步数的方案。如果有多解,可以输出任意解。如图7-33所示,s=1,t=5时,最少需要16步:机器人1-6,石头2-1-7,机器人6-1-2-8,石头3-2-1-6,石头4-3-2-1,最后机器人8-2-3-4-5。
思路
这个题的重点是状态压缩,我的做法是记录机器人的位置以及每个位置上是否空的,来作为状态。详见代码。
主体框架是BFS,没有太复杂的东西。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
const int MAXN = 15;
struct P {
int x;
int y;
vector<int> vt;
P(){}
P(int x1, int y1, vector<int> vt1) {
x = x1, y = y1, vt = vt1;
}
};
int ks;
int n, m, s, t;
int OB[MAXN + 1], G[MAXN + 1][MAXN + 1];
int ST[MAXN][1 << MAXN]; //第一个维度表示机器人所在位置,第二个维度表示是否有障碍物(有就是1)
bool BFS() {
memset(ST, 0, sizeof(ST)); //0表示有小球,1表示空洞
int x = s, y = 0;
FOR1(i, 0, m - 1) y |= (1 << OB[i]);
y |= (1 << s);
queue<P> Q;
vector<int> vt0, vt;
Q.push(P(x, y, vt));
ST[x][y] = 1;
while (!Q.empty()) {
P p = Q.front(); Q.pop();
x = p.x;
y = p.y;
vt0 = vt = p.vt;
if (x == t) {
printf("Case %d: %d\n", ks, ST[x][y]-1);
int m = vt.size();
FOR1(i, 0, m - 1) {
printf("%d %d\n", vt[i] + 1, vt[i + 1] + 1);
i++;
}
return true;
}
FOR1(i, 0, n - 1) {
FOR1(j, 0, n - 1) {
if (G[i][j] && (y&(1<<i)) && !((y>>j)&1)) { //i位置为1,j位置为0
int x1 = x, y1 = y;
if (i == x) x1 = j;
y1 ^= (1 << i), y1 ^= (1 << j); //i和j位置取反
if (!ST[x1][y1]) {
vt = vt0;
vt.push_back(i), vt.push_back(j);
Q.push(P(x1, y1, vt));
ST[x1][y1] = ST[x][y] + 1;
}
}
}
}
}
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int kase;
cin >> kase;
for (ks = 1; ks <= kase; ks++) {
cin >> n >> m >> s >> t;
s--, t--;
FOR1(i, 0, m - 1) {
cin >> OB[i];
OB[i]--;
}
int a, b;
memset(G, 0, sizeof(G));
FOR1(i, 1, n - 1) {
cin >> a >> b;
G[a-1][b-1] = G[b-1][a-1] = 1;
}
if (!BFS())
printf("Case %d: -1\n", ks);
printf("\n");
}
return 0;
}
习7-12 UVA 1533 移动小球
题意
如图7-34所示,一共有15个洞,其中一个空着,剩下的洞里各有一个小球。每次可以让一个小球越过同一条直线上的一个或多个连续的小球,落到最近的空洞(不能越过空洞),然后拿走被跳过的小球。例如,让14跳到空洞5中,则洞9里的小球会被拿走,因此操作之后洞9和14会变空,而5里面会有一个小球。你的任务是用最少的步数让整个棋盘只剩下一个小球,并且位于初始时的那个空洞中。
输入仅包含一个整数,即空洞编号,输出最短序列的长度m,然后是m个整数对,分别表示每次跳跃的小球所在的洞编号以及目标洞的编号。
思路
典型的状态压缩DP+BFS。
主要是状态压缩,我的做法是用二级制记录15个洞是否有小球的状态。然后用BFS状态转移即可。
其实整体框架BFS跟上一个题目是一样的。
需要注意的两个地方:
1、小技巧:二进制数可以用异或操作来转换状态。
2、注意输出要按照字典序最小原则。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
typedef pair<int, vector<int> > P;
const int INF = 0x3f3f3f3f;
const int MAXN = 65536;
int n;
int T[6][6];
int ST[1 << 15];
int d[6][2] = { { -1, -1 }, { -1, 0 }, { 0, -1 }, { 0, 1 }, { 1, 0 }, { 1, 1 } }; //注意要按照字典序最小来输出,因而这里的顺序就有讲究
void pre_process() {
int cnt = 0;
FOR1(i, 0, 4) {
FOR1(j, 0, i) {
T[i][j] = cnt++;
}
}
}
bool legal(int x, int y) {
return 0 <= x && x <= 4 && 0 <= y && y <= x;
}
bool BFS(int x) {
x--;
int G0[6][6], G[6][6]; //用于转换
memset(ST, 0, sizeof(ST)); //0表示有小球,1表示空洞
queue<P> Q;
vector<int> vt0, vt;
Q.push(P(1 << x, vt));
ST[1 << x] = 1;
while (!Q.empty()) {
P p = Q.front(); Q.pop();
int a = p.first;
vt0 = vt = p.second;
if (a == (1 << 15) - 1 - (1 << x)) {
printf("%d\n", ST[a] - 1);
int m = vt.size();
FOR1(i, 0, m - 2)
printf("%d ", vt[i] + 1);
printf("%d\n", vt[m-1] + 1);
return true;
}
int cnt = 0;
FOR1(i, 0, 4) {
FOR1(j, 0, i) {
G0[i][j] = G[i][j] = (a >> (cnt++)) & 1;
}
}
int ni, nj;
FOR1(i, 0, 4) {
FOR1(j, 0, i) {
if (!G0[i][j]) {
FOR1(k, 0, 5) {
memcpy(G, G0, sizeof(G));
ni = i, nj = j;
int cnt = 0;
do {
ni = ni + d[k][0], nj = nj + d[k][1];
cnt++;
} while (legal(ni, nj) && !G[ni][nj]);
if (cnt >= 2 && legal(ni, nj)) { //找到可跳方案
int b = a;
ni = i, nj = j;
FOR1(r, 0, cnt - 1) {
b |= (1 << T[ni][nj]);
ni = ni + d[k][0], nj = nj + d[k][1];
};
b &= ~(1 << T[ni][nj]);
if (!ST[b]) {
vt = vt0;
vt.push_back(T[i][j]), vt.push_back(T[ni][nj]);
Q.push(P(b, vt));
ST[b] = ST[a] + 1;
}
}
}
}
}
}
}
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
pre_process();
int kase;
cin >> kase;
FOR1(ks, 1, kase) {
int x;
cin >> x;
if (!BFS(x))
printf("IMPOSSIBLE\n");
}
return 0;
}
习7-13 UVA 817 数字表达式*(未通过,RE)
题意
输入一个以等号结尾、前面只包含数字的表达式,插入一些加号、减号和乘号,使得运算结果等于2000。表达式里的整数不能有前导零(例如,0100或者000都是非法的),运算符都是二元的(例如,2*-100*-10+0=是非法的),并且符合通常的运算优先级法则。
输入数字个数不超过9。如果有多解,按照字典序从小到大输出;如果无解,输出IMPOSSIBLE。例如,2100100=有3组解,按照字典序依次为210010+0=、210010-0=和2100-100=。
思路
基本的DFS,不过有很多细节需要注意。比如前导零的判断。
我的代码已经通过了所给的测试用例以及udebug上的测试用例,但是提交后RE了,正在查找原因。
另外特别需要注意:
1、表达式2000=的输出是IMPOSSIBLE。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <string>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
const int MAXN = 65536;
int n;
string S;
vector<string> res;
int comb(int a, char sig, int b) {
if (sig == '+') return a + b;
if (sig == '-') return a - b;
return a * b;
}
bool check(string T) {
int c = T.size();
int a = 0, b = 0;
char sig = '+';
int i = 0;
//cout << T << endl;
while (i < c) {
if (i == 0 || T[i] == '+' || T[i] == '-') {
if (i) {
a = comb(a, sig, b);
sig = T[i++];
}
b = 0;
while (T[i] >= '0' && T[i] <= '9') {
b = b * 10 + (T[i] - 48);
i++;
}
}
else {
i++;
int b1 = 0;
while (T[i] >= '0' && T[i] <= '9') {
b1 = b1 * 10 + (T[i] - 48);
i++;
}
b = comb(b, '*', b1);
}
}
if (comb(a, sig, b) == 2000) {
res.push_back(T);
return true;
}
return false;
}
bool DFS(int k, string T) {
if (k == n - 1) return check(T);
int t = T.size();
if (t == 1) {
if (T[0] != '0') DFS(k + 1, T + S[k]);
}
else {
if (!(T[t - 1] == '0' && (T[t - 2] < '0' || T[t - 2] > '9')))
DFS(k + 1, T + S[k]);
}
DFS(k + 1, T + '+' + S[k]);
DFS(k + 1, T + '-' + S[k]);
DFS(k + 1, T + '*' + S[k]);
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int kase = 0;
while (cin >> S && S[0] != '=') {
n = S.size();
res.clear();
DFS(1, S.substr(0, 1));
sort(res.begin(), res.end());
printf("Problem %d\n", ++kase);
if (res.size()) {
FOR1(i, 0, res.size() - 1)
cout << " " << res[i] << '=' << endl;
}
else printf(" IMPOSSIBLE\n");
}
return 0;
}
习7-14 UVA 307 小木棍
题意
乔治有一些同样长的小木棍,他把这些木棍随意地砍成几段,直到每段的长度都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己最开始时有多少根木棍和它们的分别长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。例如,若砍完后有4根,长度分别为1, 2, 3, 4,则原来可能是2根长度为5的木棍,也可能是1根长度为10的木棍,其中5是最小可能长度。另一个例子是:砍之后的木棍有9根,长度分别为5, 2, 1, 5, 2, 1, 5, 2, 1,则最小可能长度为6(5+1=5+1=5+1=2+2+2=6),而不是8(5+2+1=8)。
思路
很容易想到根据木棍长度枚举,然后用DFS找答案。关键是怎么判断。
首先要把木棍长度按照从大到小排序,然后按需搜索。
我参考了别人的博客,有这样两个重要的剪枝:
1、最长的木棍肯定要用到,如果用不到,这说明此方案失败,返回。
2、相同长度的棍子,如果某一个没用到,则下一个相同长度的也用不到。这一点在我代码里没写,实际可以加。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
const int INF = 0x3f3f3f3f;
const int MAXN = 65536;
int n;
int A[MAXN], V[MAXN], tot;
bool DFS(int s, int left, int L, int cnt) {
if (!cnt) return true;
FOR1(i, s, n - 1) {
if (!V[i] && A[i] < left) {
V[i] = 1;
if (DFS(i + 1, left - A[i], L, cnt)) return true;
V[i] = 0;
if (left == L) return false; //说明第一个棒没用到,这就没必要继续搜了
}
if (!V[i] && A[i] == left) {
V[i] = 1;
if (DFS(0, L, L, cnt - 1)) return true;
V[i] = 0;
return false; //说明剩下的棒没成功,直接失败
}
}
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
while (cin >> n && n) {
tot = 0;
FOR1(i, 0, n - 1) {
cin >> A[i];
tot += A[i];
}
sort(A, A + n, greater<int>());
FOR1(L, A[0], tot) {
if (tot % L) continue;
memset(V, 0, sizeof(V));
if (DFS(0, L, L, tot / L)) {
printf("%d\n", L); break;
}
}
}
return 0;
}
习7-15 UVA 11882 最大的数
题意
在一个R行C列(2≤R,C≤15,R*C≤30)的矩阵里有障碍物和数字格(包含1~9的数字)。你可以从任意一个数字格出发,每次沿着上下左右之一的方向走一格,但不能走到障碍格中,也不能重复经过一个数字格,然后把沿途经过的所有数字连起来,如图7-35所示。
如图7-35可以得到9784、4832145等整数。问:能得到的最大整数是多少?
思路
这是别人的AC代码。有两大亮点:
1、假设已经找到的答案数组是b,目前尝试的数组是c,当前要填的位置是cur。答案的长度是maxd,如果b,c两数组在cur之前的所有数字均相等,但当前要填的值val<b[cur],那么可想而知,c的结果一定小于b,此时再往下寻找结果也不会更优,直接剪枝!
2、另外,通过maxd和当前位置cur可以知道还需要寻找的数字个数是res。如果当前填入的值val对应的坐标是(x,y),用一个find(x,y)函数表示它后面最多还能找到的数字个数。可想而知,当find(x,y)<res时,即往下能够找到的最多的数字个数还达不到最低要求时,需要剪枝。而find函数还可以通过递归来实现,详细细节见代码。
原文链接:https://blog.csdn.net/u014800748/article/details/45128759
由于我在思想上都理解了,所以暂时先不自己写代码了。
代码
习7-16 UVA 11846 找座位(未通过,TLE)
题意
有一个n*n(n<20)的座位矩阵里坐着k(k≤26)个研究小组。每个小组的座位都是矩形形状。输入每个小组组长的位置和该组的成员个数,找到一种可能的座位方案。如图7-36所示是一组输入和对应的输出。
思路
显然是DFS。但是我根据小组来找,超时了,后来又加了一些优化,应该是降低了2个数量级,还是超时。可能还需要降低1个数量级才行,但我目前的代码不太好优化了。
小伙伴可以参考这篇博客:https://blog.csdn.net/qq_36973725/article/details/86185025
代码
习7-17 UVA 11694 Gokigen Naname 谜题
题意
在一个n*n(n≤7)网格中,有些交叉点上有数字。你的任务是给每个格子画一条斜线(一共只有“\”和“/”两种),使得每个交叉点的数字等于和它相连的斜线条数,且这些斜线不会构成环,如图7-37所示。
思路
稍微复杂一些的DFS,我的做法是从上到下从左到右逐个搜索,并在每一个行放置完成时判断是否形成环。
判断环的经典做法是并查集。
代码中需要特别留意的是数组的恢复,其中我在并查集恢复中犯了细节错误,调试了很长时间才成功。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <queue>
#include <vector>
using namespace std;
#define FOR1(i, a, b) for (int i = (a); i <= (int)(b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (int)(b); i--)
int n; //长方形的边长
int C[10][10], nowC[10][10]; //表示斜线条数
char S[10][10]; //放置斜线的字符数组
int pre[100]; //并查集,用于判圈
bool legal(int a, int b) {
if (a != -1 && b != a) return false;
return true;
}
bool combine(int a, int b) {
int ka = 0, kb = 0;
while (a != pre[a]) {
a = pre[a];
ka++;
}
while (b != pre[b]) {
b = pre[b];
kb++;
}
if (a == b) return false;
if (ka > kb) pre[b] = a;
else pre[a] = b;
return true;
}
bool DFS(int x, int y) { //当前搜索到x,y位置
if (x && y == 0) { //说明某一行已经赋值完毕,判断环,需要用并查集(注意要保存前n行的赋值,以便回溯)。一开始这个地方忽略了一种特殊情况,提交后WA了。
if (x == 2)
x = 2;
if (x == 3)
x = 3;
int n1 = n + 1;
FOR1(j, 0, n) pre[x*n1 + j] = x*n1 + j; //初始化新行
FOR1(j, 0, n - 1) {
if (S[x - 1][j] == '/') {
if (!combine(x*n1 + j, (x - 1)*n1 + j + 1)) //说明有环
return false;
}
else {
if (!combine(x*n1 + j + 1, (x - 1)*n1 + j)) //说明有环
return false;
}
}
}
if (x == n) { //说明已经搜索完毕
FOR1(i, 0, n) { //判断最后一行的斜线条数是否正确
if (!legal(C[n][i], nowC[n][i])) return false;
}
FOR1(i, 0, n - 1) printf("%s\n", S[i]);
return true;
}
int nx = x, ny = y + 1;
if (ny == n) nx++, ny = 0;
int pre1[100]; //注意这里的pre1一定要设为局部变量,全局变量会出毛病
if (legal(C[x][y], nowC[x][y])) {
if (!(ny == 0 && !legal(C[x][n], nowC[x][n] + 1))) {
S[x][y] = '/';
nowC[x][y + 1]++, nowC[x + 1][y]++;
if (ny == 0) memcpy(pre1, pre, sizeof(pre)); //备份并查集
if (DFS(nx, ny)) return true;
nowC[x][y + 1]--, nowC[x + 1][y]--; //还原计数
if (ny == 0) memcpy(pre, pre1, sizeof(pre)); //还原并查集
}
}
if (legal(C[x][y], nowC[x][y] + 1)) {
if (!(ny == 0 && !legal(C[x][n], nowC[x][n]))) {
S[x][y] = '\\';
nowC[x][y]++, nowC[x + 1][y + 1]++;
if (ny == 0) memcpy(pre1, pre, sizeof(pre)); //备份并查集
if (DFS(nx, ny)) return true;
nowC[x][y]--, nowC[x + 1][y + 1]--; //还原
if (ny == 0) memcpy(pre, pre1, sizeof(pre)); //还原并查集
}
}
return false;
}
int main() {
#ifdef CODE_LIANG
freopen("datain.txt", "r", stdin);
freopen("dataout.txt", "w", stdout);
#endif
int kase;
cin >> kase;
FOR1(i, 1, kase) {
scanf("%d", &n);
char C1[10][10];
FOR1(i, 0, n) {
scanf("%s", C1[i]);
FOR1(j, 0, n) {
if (C1[i][j] == '.') C[i][j] = -1;
else C[i][j] = C1[i][j] - 48;
}
}
FOR1(i, 0, n - 1) S[i][n] = '\0';
memset(nowC, 0, sizeof(nowC));
FOR1(j, 0, n) pre[j] = j; //初始化第0行的并查集
DFS(0, 0);
}
return 0;
}
习7-18 UVA 10384 推门游戏(未尝试)
题意
如图7-38所示,从S处出发,每次可以往东、南、西、北4个方向之一前进。如果前方有墙壁,游戏者可以把墙壁往前推一格。如果有两堵或者多堵连续的墙,则不能推动。另外,游戏者也不能推动游戏区域边界上的墙。
用最少的步数走出迷宫(边界处没有墙的地方就是出口)。迷宫总是有4行6列,多解时任意输出一个移动序列即可(用NEWS这4字符表示移动方向)
思路
需要用IDA*算法。
代码