文章目錄
- 說明
- 習題
- 習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*算法。
代碼