算法竞赛入门经典(第二版)-刘汝佳-第七章 暴力求解法 习题(11/18)

说明

本文是我对第七章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*算法。
代码



  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值