文章目录
- 说明
- 例题
- 例6-1 UVA 210 并行程序模拟 (p139, 双端队列)
- 例6-2 UVA 514 铁轨 (p140, 栈)
- 例6-3 UVA 442 矩阵链乘 (p141, 用栈实现简单的表达式解析)
- 例6-4 UVA 11988 破损的键盘 (p143, 链表)
- 例6-5 UVA 12657 移动盒子 (p144, 双向链表)
- 例6-6 UVA 679 小球下落 (p148, 完全二叉树编号)
- 例6-7 UVA 122 树的层次遍历 (p150, 二叉树的动态创建于 BFS)
- 例6-8 UVA 548 树 (p155, 从中序和后续回复二叉树)
- 例6-9 UVA 839 天平 (p157, 二叉树的 DFS)
- 例6-10 UVA 699 下落的树叶 (p159, 二叉树的 DFS)
- 例6-11 UVA 297 四分树 (p160)
- 例6-12 UVA 572 油田 (p162, 图的连通块 DFS)
- 例6-13 UVA 1103 古代象形符号 (p163, 图的连通块的应用)
- 例6-14 UVA 816 Abbott 的复仇 (p165, 图的连通块 BFS)
- 例6-15 UVA 10305 按任务排序 (p167, 拓扑排序)
- 例6-16 UVA 10129 单词 (p169, 欧拉回路)
- 例6-17 UVA 10562 看图写数 (p170, 多叉数 DFS)
- 例6-18 UVA 12171 雕塑 (p171, 离散化: floodfill)(未尝试)
- 例6-19 UVA 1572 自组合 (p172, 图论模型)
- 例6-20 UVA 1599 理想路径 (p173, 图的 BFS 树)(未尝试)
- 例6-21 UVA 506 系统依赖 (p173, 图的概念与拓扑排序)(未尝试)
- 例6-22 UVA 11853 战场 (p175, 对偶图)(未尝试)
说明
本文是我对第六章22道例题的练习总结,建议配合紫书——《算法竞赛入门经典(第2版)》阅读本文。
另外为了方便做题,我在VOJ上开了一个contest,欢迎一起在上面做:第六章例题contest
如果想直接看某道题,请点开目录后点开相应的题目!!!
例题
例6-1 UVA 210 并行程序模拟 (p139, 双端队列)
思路
这个题做了得有五六个小时吧,RE了3次,WA了一次。这种模拟题总感觉在理解上有歧义,也许有人一上来就理解对了,但我上来就没理解对(也可能因为原题描述不清楚),于是就一直没有正确的思路。
书中给出的题意是这样:
你的任务是模拟n个程序(按输入顺序编号为1~n)的并行执行。每个程序包含不超过25条语句,格式一共有5种:var = constant(赋值);print var(打印);lock;unlock;end。
变量用单个小写字母表示,初始为0,为所有程序公有(因此在一个程序里对某个变量赋值可能会影响另一个程序)。常数是小于100的非负整数。
每个时刻只能有一个程序处于运行态,其他程序均处于等待态。上述5种语句分别需要t1、t2、t3、t4、t5单位时间。运行态的程序每次最多运行Q个单位时间(称为配额)。当一个程序的配额用完之后,把当前语句(如果存在)执行完之后该程序会被插入一个等待队列中,然后处理器从队首取出一个程序继续执行。初始等待队列包含按输入顺序排列的各个程序,但由于lock/unlock语句的出现,这个顺序可能会改变。
lock的作用是申请对所有变量的独占访问。lock和unlock总是成对出现,并且不会嵌套。lock总是在unlock的前面。当一个程序成功执行完lock指令之后,其他程序一旦试图执行lock指令,就会马上被放到一个所谓的阻止队列的尾部(没有用完的配额就浪费了)。当unlock执行完毕后,阻止队列的第一个程序进入等待队列的首部。
输入n, t1, t2, t3, t4, t5, Q以及n个程序,按照时间顺序输出所有print语句的程序编号和结果。
我对题意补充一些说明:
1、变量始终为所有程序公有,根本没有某个程序对于变量的私有化处理,也就是说所谓的独占访问跟变量公有不矛盾。
2、unlock会使整个任务状态解除锁定。
3、执行完lock指令之后其他程序试图执行lock指令时,这个lock指令此时不会被执行。(我这个地方理解错了)
关于我编程中所犯错误的小结:
1、lock指令的处理不当,参考前面说明。
2、unlock时,阻止队列中并不一定有程序,这时应该进行判断(RE的原因)。
另外补充几个问题:
1、题目示例INPUT漏掉了测试数据组数。
2、最后一组数据后不应该输出空行。
最后,给出一组测试数据,供查错参考:
INPUT:
1
3 1 1 1 1 1 3
a = 5
print a
end
b = 7
lock
print a
print b
unlock
c = 10
print a
end
print a
print b
lock
print c
unlock
end
OUTPUT:
1: 5
2: 5
3: 5
3: 7
2: 7
3: 10
2: 5
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<sstream>
#include<algorithm>
#include<queue>
#include<deque>
#include<map>
using namespace std;
const int N = 1001;
int n, qt, t[5];
queue<string> prog[N];
deque<int> dq;
queue<int> que;
map<string, int> mp;
int cur, passed, locked;
void init_data()
{
for (int i = 0; i < n; i++)
while (prog[i].size()) prog[i].pop();
while (dq.size()) dq.pop_front();
while (que.size()) que.pop();
mp.clear();
}
void run_prog()
{
cur = dq.front();
dq.pop_front();
passed = 0;
while (passed < qt && prog[cur].size()) {
string s = prog[cur].front();
//cout << cur+1 << "----------" << s << endl;
string name, tmp;
int val;
if (s.find('=') != string::npos) {
stringstream ss(s);
ss >> name >> tmp >> val;
mp[name] = val;
passed += t[0];
} else if (s.find("print") != string::npos) {
stringstream ss(s);
ss >> tmp >> name;
printf("%d: %d\n", cur+1, mp[name]);
passed += t[1];
} else if (s == "lock") {
if (locked) {
que.push(cur);
return;
}
locked = 1;
passed += t[2];
} else if (s == "unlock") {
locked = 0;
if (que.size()) {
dq.push_front(que.front());
que.pop();
}
passed += t[3];
} else if (s == "end") {
return;
}
prog[cur].pop();
}
dq.push_back(cur);
}
int main()
{
int kase;
scanf("%d", &kase);
while (kase --) {
scanf("%d", &n);
for (int i = 0; i < 5; i++)
scanf("%d", &t[i]);
scanf("%d", &qt);
getchar();
init_data();
string s;
for (int i = 0; i < n; i++) {
do {
getline(cin, s);
prog[i].push(s);
} while (s != "end");
dq.push_back(i);
}
locked = 0;
while (dq.size()) {
run_prog();
//printf("passed = %d, dq.size = %d\n", passed, dq.size());
}
if (kase) printf("\n");
}
return 0;
}
例6-2 UVA 514 铁轨 (p140, 栈)
思路
先进后出为栈,详细分析见书中例题部分。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
using namespace std;
const int N = 1000;
int n, a[N+1];
stack<int> s;
int main(void)
{
while (cin >> n && n) {
while (cin >> a[1] && a[1]) {
for (int i = 2; i <= n; i ++)
scanf("%d", &a[i]);
int i = 1, j = 1;
while (i <= n) {
while (!s.empty() && s.top() == a[i]) {
s.pop(); i ++;
}
if (j <= n) s.push(j++);
else break;
}
if (i > n) puts("Yes");
else puts("No");
}
cout << endl;
}
return 0;
}
例6-3 UVA 442 矩阵链乘 (p141, 用栈实现简单的表达式解析)
思路
这个题的输入表达式非常标准,两个矩阵用一对括号包起来。如下面所示:
A
B
C
(AA)
(AB)
(AC)
(A(BC))
((AB)C)
(((((DE)F)G)H)I)
(D(E(F(G(HI)))))
((D(EF))((GH)I))
所以写代码的时候不用考虑太多,按照这样的标准格式递归处理即可。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <string>
using namespace std;
typedef pair<int, int> P;
P p[26];
stack<P> st;
int main(void)
{
int n;
cin >> n;
char ch[2];
int x, y;
for (int i = 0; i < n; i ++) {
scanf("%s%d%d", ch, &x, &y);
p[ch[0]-'A'] = P(x, y);
}
string s;
while (cin >> s) {
while (st.size()) st.pop();
int res = 0;
for (int i = 0; i < s.size(); i ++) {
if (s[i] == ')') {
P b = st.top(); st.pop();
P a = st.top(); st.pop();
if (a.second != b.first) {
res = -1; break;
} else {
res += (a.first * a.second * b.second);
st.push(P(a.first, b.second));
}
} else if (s[i] != '('){
st.push(p[s[i]-'A']);
}
}
if (res == -1) puts("error");
else printf("%d\n", res);
}
return 0;
}
例6-4 UVA 11988 破损的键盘 (p143, 链表)
思路
链表用数组来写更简单。
还有一个小技巧是头节点为数组的第一个元素,且头节点是虚拟节点,这样便于插入删除操作的处理。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 100000;
char s[N+2];
int next[N+2];
int main(void)
{
while (scanf("%s", s+1) != EOF) {
int cur = 0, last = 0;
next[0] = 0;
for (int i = 1; s[i]; i ++) {
if (s[i] == '[') cur = 0;
else if (s[i] == ']') cur = last;
else {
next[i] = next[cur];
next[cur] = i;
if (cur == last) last = i;
cur = i;
}
}
for (int i = next[0]; i != 0; i = next[i])
printf("%c", s[i]);
printf("\n");
}
return 0;
}
例6-5 UVA 12657 移动盒子 (p144, 双向链表)
思路
双向链表的操作要比单向链表复杂一倍,写的时候注意以下几点:
1、反转用一个全局变量表示即可,不需要做实际反转操作。
2、指令1 2 3有重复模块,可以用辅助函数实现精简代码。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100000;
int n, next[N+1][2];
int d = 0;
void init()
{
for (int i = 0; i < n; i ++) {
next[i][0] = i+1;
next[i+1][1] = i;
}
next[n][0] = 0;
next[0][1] = n;
d = 0;
}
int lt(int a)
{
return next[a][(d+1)&1];
}
int rt(int a)
{
return next[a][d];
}
void set_lt(int a, int b)
{
next[a][(d+1)&1] = b;
}
void set_rt(int a, int b)
{
next[a][d] = b;;
}
void link(int a, int b)
{
set_rt(a, b);
set_lt(b, a);
}
void move_left(int a, int b)
{
link(lt(a), rt(a));
link(lt(b), a);
link(a, b);
}
void move_right(int a, int b)
{
link(lt(a), rt(a));
link(a, rt(b));
link(b, a);
}
void swap_pos(int a, int b)
{
if (rt(a) == b)
move_right(a, b);
else if (lt(a) == b)
move_left(a, b);
else {
int la = lt(a), ra = rt(a);
link(lt(b), a);
link(a, rt(b));
link(la, b);
link(b, ra);
}
}
void print_array()
{
int a = 0;
while (a = rt(a))
printf("%d ", a);
printf("\n");
}
long long sum()
{
int a = 0;
long long res = 0;
while (a = rt(a)) {
res += a;
if (!(a = rt(a))) break;
}
return res;
}
int main(void)
{
int t = 0;
while (cin >> n) {
init();
int m;
cin >> m;
int a, b, c;
for (int i = 1; i <= m; i ++) {
scanf("%d", &a);
switch(a) {
case 1:
scanf("%d%d", &b, &c);
move_left(b, c);
break;
case 2:
scanf("%d%d", &b, &c);
move_right(b, c);
break;
case 3:
scanf("%d%d", &b, &c);
swap_pos(b, c);
break;
case 4:
d = (d+1)%2;
break;
}
}
//print_array();
printf("Case %d: %lld\n", ++t, sum());
}
return 0;
}
例6-6 UVA 679 小球下落 (p148, 完全二叉树编号)
思路
开始模拟小球整个运动过程写的,果断超时了。
后来发现第I个小球的运动轨迹可以直接根据I求出来,详见书中解释。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int main(void)
{
int t;
while (cin >> t && t >= 0) {
while (t --) {
int d, k;
cin >> d >> k;
int n = (1<<d);
int r = 1;
while (r < n/2) {
if (k&1) { r = 2*r; k = k/2+1;}
else { r = 2*r+1; k = k/2;}
}
printf("%d\n", r);
}
}
return 0;
}
例6-7 UVA 122 树的层次遍历 (p150, 二叉树的动态创建于 BFS)
思路
两种方法:
1、宽度优先遍历。
2、二叉树递归遍历,每层的节点存入对应层的vector数组中。
下面的代码实现了方法1。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
struct Node {
int k;
Node *l, *r;
};
Node* prase(Node *root, int k, char s[300])
{
if (!root) {
root = (Node *)malloc(sizeof(Node));
root->k = 0;
root->l = root->r = NULL;
}
if (s[0] == 'L') root->l = prase(root->l, k, s+1);
else if (s[0] == 'R') root->r = prase(root->r, k, s+1);
else {
if (root->k == 0) root->k = k;
else root->k = -1;
}
return root;
}
bool bfs(Node *root, vector<int>& res)
{
queue<Node*> q;
q.push(root);
while (q.size()) {
Node *p = q.front();
q.pop();
if (p->k <= 0) return false;
res.push_back(p->k);
if (p->l) q.push(p->l);
if (p->r) q.push(p->r);
}
return true;
}
int main(void)
{
char str[300], str1[300];
while (scanf("%s", str) != EOF) {
Node *root = NULL;
do {
if (strcmp(str, "()") == 0) break;
int k;
sscanf(str, "(%d", &k);
int len = 0;
for (char *ch = strchr(str, ',')+1; ch < strchr(str, ')'); ch ++)
str1[len++] = *ch;
str1[len] = '\0';
root = prase(root, k, str1);
} while (scanf("%s", str));
vector<int> res;
if (bfs(root, res)) {
for (int i = 0; i < res.size(); i ++)
printf("%d%c", res[i], (i == res.size()-1) ? '\n' : ' ');
} else
puts("not complete");
}
return 0;
}
例6-8 UVA 548 树 (p155, 从中序和后续回复二叉树)
思路
后序遍历的第一个字符就是根,因此只需在中序遍历中找到它,就知道左右子树的中序和后序遍历了。这样可以先把二叉树构造出来,然后再执行一次递归遍历,找到最优解。
代码
#include <iostream>
#include <sstream>
#include <cstdio>
#include <algorithm>
#include <string>
using namespace std;
const int N = 10000;
int lt[N+1], rt[N+1], sum[N+1];
int min_value, min_sum;
int rebuild(int n, int in[], int post[], int sum)
{
if (n == 0) return 0;
int root = post[n-1];
sum += root;
if (n == 1) {
if (sum < min_sum || sum == min_sum && root < min_value) {
min_value = root;
min_sum = sum;
}
lt[root] = rt[root] = 0;
return root;
}
int m;
for (m = 0; m < n; m ++) {
if (in[m] == root) break;
}
lt[root] = rebuild(m, in, post, sum);
rt[root] = rebuild(n-m-1, in+m+1, post+m, sum);
return root;
}
int main(void)
{
int n;
int in[N+1], post[N+1];
string s;
while (getline(cin, s)) {
n = 0;
stringstream stm(s);
while (stm >> in[n]) n ++;
for (int i = 0; i < n; i ++)
cin >> post[i];
getchar();
min_value = min_sum = N*N;
rebuild(n, in, post, 0);
printf("%d\n", min_value);
}
return 0;
}
例6-9 UVA 839 天平 (p157, 二叉树的 DFS)
思路
这个题输入既然采用了递归的形式遍历,编一个递归过程处理输入比较自然。事实上,再输入的过程中就能够完成判断。
我这个题做了多余的建树操作,实际上没有必要。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
struct Node {
int w, d;
Node *lt, *rt;
Node():w(0),d(0),lt(NULL),rt(NULL){}
};
bool res;
void input(Node *root)
{
Node *l = root->lt = new Node();
Node *r = root->rt = new Node();
scanf("%d%d%d%d", &l->w, &l->d, &r->w, &r->d);
if (!l->w) input(l);
if (!r->w) input(r);
if (!root->w) root->w = l->w + r->w;
if (l->w * l->d != r->w * r->d)
res = false;
}
void deltree(Node *root)
{
if (!root) return;
if (root->lt) deltree(root->lt);
if (root->rt) deltree(root->rt);
delete root;
}
int main(void)
{
int kase;
cin >> kase;
for (int t = 0; t < kase; t ++) {
if (t) puts("");
res = true;
Node *root = new Node();
input(root);
deltree(root);
if (res) puts("YES");
else puts("NO");
}
return 0;
}
例6-10 UVA 699 下落的树叶 (p159, 二叉树的 DFS)
思路
以int sum[2*SFT]表示各水平位置的值的和。根节点对应位置为SFT,每个位置为k的节点的左节点位置为k-1,右节点位置为k+1。
其实就是个递归,很简单。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int SFT = 100;
int sum[2*SFT];
struct Node {
int k, sft;
Node *lt, *rt;
Node():k(0),lt(NULL),rt(NULL){}
};
Node* input(int sft)
{
int k;
scanf("%d", &k);
if (k == -1) return NULL;
Node *p = new Node();
p->k = k;
sum[sft] += k;
p->lt = input(sft-1);
p->rt = input(sft+1);
return p;
}
void print_sum()
{
int i;
for (i = 0; i < 2*SFT; i ++) {
if (sum[i]) { printf("%d", sum[i++]); break; }
}
while (sum[i]) {
printf(" %d", sum[i++]);
}
printf("\n");
}
void deltree(Node *root)
{
if (!root) return;
if (root->lt) deltree(root->lt);
if (root->rt) deltree(root->rt);
delete root;
}
int main(void)
{
int t = 0;
while (true) {
memset(sum, 0, sizeof(sum));
Node *root = input(SFT);
if (!root) break;
printf("Case %d:\n", ++t);
print_sum();
printf("\n");
deltree(root);
}
return 0;
}
例6-11 UVA 297 四分树 (p160)
思路
跟二叉树极其类似,都是基于递归思想来处理输入。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
void input(int x0, int y0, int x1, int y1, char area[32][32])
{
char ch = getchar();
if (ch == 'f') {
for (int i = x0; i < x1; i ++)
for (int j = y0; j < y1; j ++)
area[i][j] = '1';
} else if (ch == 'p') {
int mx = (x0+x1) / 2;
int my = (y0+y1) / 2;
input(x0, my, mx, y1, area);
input(x0, y0, mx, my, area);
input(mx, y0, x1, my, area);
input(mx, my, x1, y1, area);
}
}
int cnt(char area[2][32][32])
{
int res = 0;
for (int i = 0; i < 32; i ++) {
for (int j = 0; j < 32; j ++) {
if (area[0][i][j] == '1' || area[1][i][j] == '1')
res ++;
}
}
return res;
}
int main(void)
{
int t;
cin >> t;
while (t--) {
char area[2][32][32];
memset(area, '0', sizeof(area));
for (int i = 0; i < 2; i ++) {
getchar();
input(0, 0, 32, 32, area[i]);
}
printf("There are %d black pixels.\n", cnt(area));
}
return 0;
}
例6-12 UVA 572 油田 (p162, 图的连通块 DFS)
思路
最基本的DFS,注意是八连通不是四连通。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 100
int n, m;
char lake[N][N+1];
void dfs(int x, int y)
{
lake[x][y] = '*';
for (int dx = -1; dx <= 1; dx ++) {
for (int dy = -1; dy <= 1; dy ++) {
if (!dx && !dy)
continue;
int nx = x + dx, ny = y + dy;
if (0 <= nx && nx < n && 0 <= ny && ny < m
&& lake[nx][ny] == '@')
dfs(nx, ny);
}
}
}
void solve() {
int res = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (lake[i][j] == '@') {
dfs(i, j);
res ++;
}
}
}
printf("%d\n", res);
}
int main(void)
{
while (cin >> n >> m, n || m) {
for (int i = 0; i < n; i++)
cin >> lake[i];
solve();
}
return 0;
}
例6-13 UVA 1103 古代象形符号 (p163, 图的连通块的应用)
思路
这个题是含有洞的连通块。注意到:6个符号不管怎么变形,其拓扑上分别有1 3 5 4 0 2个洞,可以根据这一点来判断。
那么如何判断有多少个洞呢?我的程序的处理思路是:
1、初始状态下,0表示白点,1表示黑点;
2、用DFS对不同的连通块进行编号,将1分别修改为2 3 4 …,不同的非零数字代表不同的连通块;
3、用DFS将与外周连通起来的为0的区域修改为1,这时候所有为0的区域代表不同的洞;
4、用DFS数洞,判断洞的归属可以基于其DFS寻找到的大于1的连通点的数字。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 200
#define M 50
const char code[] = "WAKJSD";
int n, m;
char table[N][M+1];
int type[N][4*M];
int num;
int area[N*M];
bool legal(int x, int y)
{
return 0 <= x && x < n && 0 <= y && y < m;
}
void dfs(int x, int y, int a, int b) //change a to b
{
type[x][y] = b;
for (int dx = -1; dx <= 1; dx ++) {
for (int dy = -1; dy <= 1; dy ++) {
if (!dx && !dy) continue;
int nx = x + dx, ny = y + dy;
if (legal(nx, ny) && type[nx][ny] == a)
dfs(nx, ny, a, b);
}
}
}
void trans()
{
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int x = table[i][j] - '0';
if (isalpha(table[i][j]))
x = table[i][j] - 'a' + 10;
int r = 3;
while (x) {
type[i][4*j+r] = (x&1);
r --;
x >>= 1;
}
}
}
}
int check_nb(int x, int y)//检查是否为0且邻居不小于2
{
if (!legal(x, y) || type[x][y] != 0) return 0;
for (int dx = -1; dx <= 1; dx ++) {
for (int dy = -1; dy <= 1; dy ++) {
if (!dx && !dy) continue;
int nx = x + dx, ny = y + dy;
if (legal(nx, ny) && type[nx][ny] >= 2)
return type[nx][ny];
}
}
return 0;
}
int main(void)
{
int t = 0;
while (cin >> n >> m, n || m) {
for (int i = 0; i < n; i++)
cin >> table[i];
memset(type, 0, sizeof(type));
trans();
m *= 4;
num = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (type[i][j] == 1) {
num ++;
dfs(i, j, 1, num+1);
}
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!(i == 0 || i == n-1 || j == 0 || j == m-1)) continue;
if (type[i][j] == 0) {
dfs(i, j, 0, 1);
}
}
}
memset(area, 0, sizeof(area));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int nb = check_nb(i, j);
if (nb >= 2) {
area[nb] ++;
dfs(i, j, 0, nb);
}
}
}
printf("Case %d: ", ++t);
char res[N*M];
for (int i = 0; i < num; i++)
res[i] = code[area[i+2]];
res[num] = '\0';
sort(res, res+num);
printf("%s\n", res);
}
return 0;
}
例6-14 UVA 816 Abbott 的复仇 (p165, 图的连通块 BFS)
思路
本题与普通迷宫在本质上是一样的,区别在于“朝向”也起到了关键作用,所以要用一个三元组(r, c, dir)表示单个状态。
BFS处理过程详见代码。
代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
const int N = 9;
const int INF = 0x3f3f3f3f;
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
char towards[] = "SENW";
char turn[] = "RFL";
struct Node {
int x, y, t;
int d;
bool turn[3];
Node *pre;
};
Node map[N+1][N+1][4];
Node *bg = new Node(), *ed = new Node();
int tb = 0;
void init_map()
{
for (int i = 1; i <= N; i ++) {
for (int j = 1; j <= N; j ++) {
for (int k = 0; k < 4; k ++) {
map[i][j][k].x = i;
map[i][j][k].y = j;
map[i][j][k].t = k;
map[i][j][k].d = INF;
map[i][j][k].pre = NULL;
for (int r = 0; r < 3; r ++)
map[i][j][k].turn[r] = 0;
}
}
}
}
int trans_tow(char ch)
{
return strchr(towards, ch) - towards;
}
int trans_turn(char ch)
{
return strchr(turn, ch) - turn;
}
bool legal(int x, int y, int t)
{
return x >= 1 && x <= N && y >= 1 && y <= N && map[x][y][t].d == INF;
}
Node* next(int x, int y, int t)
{
int nx = x + dir[t][0];
int ny = y + dir[t][1];
if (legal(nx, ny, t))
return &map[nx][ny][t];
else
return NULL;
}
bool BFS()
{
Node *np = next(bg->x, bg->y, tb);
if (!np) return false;
np->pre = bg;
np->d = 1;
queue<Node*> que;
que.push(np);
while( que.size() ) {
Node *p = que.front();
que.pop();
if (p->x == ed->x && p->y == ed->y) {
ed = p;
return true;
}
for (int i = 0; i < 3; i ++) {
if (!p->turn[i]) continue;
int t = (p->t+i+3)%4;
np = next(p->x, p->y, t);
if (!np) continue;
np->pre = p;
np->d = p->d + 1;
que.push(np);
}
}
return false;
}
int main(void)
{
char name[21];
while (~scanf("%s", name) && strcmp(name, "END")) {
init_map();
char s[20];
int x, y, t;
scanf("%d%d%s", &x, &y, s);
bg = &map[x][y][0];
tb = trans_tow(s[0]);
scanf("%d%d", &x, &y);
ed = &map[x][y][0];
while (scanf("%d", &x) && x) {
scanf("%d", &y);
while (scanf("%s", s) && strcmp(s, "*")) {
t = trans_tow(s[0]);
for (int i = 1; s[i]; i ++)
map[x][y][t].turn[trans_turn(s[i])] = 1;
}
}
printf("%s\n ", name);
if (!BFS())
printf("No Solution Possible\n");
else {
vector<Node *> path;
Node *p = ed;
while (p != bg) {
p = p->pre;
path.push_back(p);
}
for (int i = path.size()-1; i >= 0; i --) {
printf("(%d,%d)%s", path[i]->x, path[i]->y,
((path.size()-i)%10) ? " " : "\n ");
}
printf("(%d,%d)\n", ed->x, ed->y);
}
}
return 0;
}
例6-15 UVA 10305 按任务排序 (p167, 拓扑排序)
思路
把每个变量看成一个点,“小于”关系看成有向边,则得到了一个有向图。这样,我们的任务实际上是把一个图的所有结点排序,使得每一条有向边(u, v)对应的u都排在v的前面。在图论中,这个问题称为拓扑排序(topological sort)。
不难发现:如果图中存在有向环,则不存在拓扑排序,反之则存在。不包含有向环的有向图称为有向无环图(Directed Acyclic Graph,DAG)。可以借助DFS完成拓扑排序:在访问完一个结点之后把它加到当前拓扑序的首部(想一想,为什么不是尾部)。
这里用到了一个c数组,c[u]=0表示从来没有访问过(从来没有调用过dfs(u));c[u]=1表示已经访问过,并且还递归访问过它的所有子孙(即dfs(u)曾被调用过,并已返回);c[u]=-1表示正在访问(即递归调用dfs(u)正在栈帧中,尚未返回)。
提示:可以用DFS求出有向无环图(DAG)的拓扑排序。如果排序失败,说明该有向图存在有向环,不是DAG。
拓扑排序实际上也可以用BFS来做,个人感觉BFS更容易理解。因为拓扑排序是访问完一个结点后加到排序的首部,而BFS则是尾部。
同时附上我写的BFS代码,已经AC。
代码1(基于DFS的拓扑排序)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 101;
int n, m;
bool G[N][N];
int c[N];
int topo[N], t;
void init()
{
memset(G, 0, sizeof(G));
memset(c, 0, sizeof(c));
memset(topo, 0, sizeof(topo));
}
bool dfs(int u)
{
c[u] = -1;
for (int k = 1; k <= n; k++) {
if (G[u][k]) {
if (c[k] < 0) return false;
else if (!c[k] && !dfs(k)) return false;
}
}
c[u] = 1;
topo[t--] = u;
return true;
}
bool toposort()
{
t = n;
for (int u = 1; u <= n; u++) {
if (!c[u]) {
if (!dfs(u)) return false;
}
}
return true;
}
int main(void)
{
while (scanf("%d%d", &n, &m) != EOF , n || m) {
init();
int a, b;
while (m--) {
scanf("%d%d", &a, &b);
G[a][b] = 1;
}
if (toposort()) {
for (int i = 1; i <= n; i++)
printf("%d%c", topo[i], i == n ? '\n' : ' ');
}
}
return 0;
}
代码2(基于BFS的拓扑排序)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int N = 101;
int n, m;
vector<int> G[N];
int pre[N];
vector<int> topo;
void init()
{
for (int i = 1; i <= n; i++)
G[i].clear();
memset(pre, 0, sizeof(pre));
}
bool toposort()
{
queue<int> q;
for (int u = 1; u <= n; u++) {
if (!pre[u]) q.push(u);
}
while (q.size()) {
int u = q.front(); q.pop();
topo.push_back(u);
//printf("u = %d\n", u);
for (int i = 0; i < G[u].size(); i++) {
int k = G[u][i];
pre[k]--;
if (pre[k] == 0)
q.push(k);
else if (pre[k] < 0)
return false;
}
}
return topo.size() == n;
}
int main(void)
{
while (scanf("%d%d", &n, &m) , n || m) {
init();
int a, b;
while (m--) {
scanf("%d%d", &a, &b);
if (a != b) {
pre[b]++;
G[a].push_back(b);
}
}
if (toposort()) {
for (int i = 0; i < topo.size(); i++)
printf("%d%c", topo[i], i == topo.size()-1 ? '\n' : ' ');
}
}
return 0;
}
例6-16 UVA 10129 单词 (p169, 欧拉回路)
思路
有向图存在欧拉回路的必要条件:最多只能有两个点的入度不等于出度,而且必须是其中一个点的出度恰好比入度大1(把它作为起点),另一个的入度比出度大1(把它作为终点)。当然,还有一个前提条件:在忽略边的方向后,图必须是连通的。
对于这个题,书中的解释是:把字母看作结点,单词看成有向边,则问题有解,当且仅当图中有欧拉路径。前面讲过,有向图存在欧拉道路的条件有两个:底图(忽略边方向后得到的无向图)连通,且度数满足上面讨论过的条件。判断连通的方法有两种,一是之前介绍过的DFS,二是第11章中将要介绍的并查集。读者可以在学习完并查集之后根据自己的喜好选用。
刘汝佳所提供的源码用的是并查集。但他同时提到用DFS也能做,于是我就想怎么用DFS来做。标准DFS过程每次搜索所有邻接节点,访问过的节点用一个数组标记。而如果DFS过程中不删掉已经访问过的边,则没遇到一个新节点都需要搜索其邻接的所有节点,岂不是多了很多无用的判断?但如果删掉访问过的边,DFS就必须从起点开始进行。
因此我的程序中,先判断节点的入度和出度是否满足条件,同时找到出度与入度之差为1的节点作为起点(如果找不到则将任意有出度的节点作为起点)。从起点进行DFS,如果访问完毕所有边都被删掉了,则说明图是连通的。
另外有几个小技巧可以提高效率:
1、读入的单词只需要将其存在vector end[26]中,见主程序中:int x = s[0]-‘a’, y = s[s.size()-1]-‘a’; end[x].push_back(y); 意思是存在一个单词,以数字x(与字符’a’之差)为首字母,数字y为末字母。这样大大降低了存储。
2、DFS访问过程中,删除的边应该从vector的末端进行。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100000;
int n;
vector<int> end[26];
int in[26], out[26];
void init_data()
{
for (int i = 0; i < 26; i++)
end[i].clear();
memset(in, 0, sizeof(in));
memset(out, 0, sizeof(out));
}
void dfs(int u)
{
while(end[u].size()) {
int len = end[u].size();
int k = end[u][len-1];
end[u].resize(len-1);
dfs(k);
}
}
bool solve()
{
//以下判断有向图的其中一个必要条件:
//最多只能有两个点的入度不等于出度,
//而且必须是其中一个点的出度恰好比入度大1,另一个小1
vector<int> deg;
int bg;
for (int i = 0; i < 26; i++) {
if (in[i] || out[i]) bg = i;
if (in[i] != out[i]) deg.push_back(i);
}
if (deg.size() == 1 || deg.size() > 2) return false;
if (deg.size() == 2) {
int i = deg[0];
if (abs(in[i]-out[i]) != 1) return false;
bg = (out[i] - in[i] == 1) ? i : deg[1];
}
//以下判断有向图欧拉路径的第二个必要条件:
//图是连通的。
dfs(bg);
for (int i = 0; i < 26; i++)
if (end[i].size()) return false;
return true;
}
int main(void)
{
int kase;
scanf("%d", &kase);
while (kase--) {
scanf("%d", &n);
init_data();
string s;
for (int i = 0; i < n; i++) {
cin >> s;
int x = s[0]-'a', y = s[s.size()-1]-'a';
end[x].push_back(y);
in[y]++;
out[x]++;
}
if (solve()) printf("Ordering is possible.\n");
else printf("The door cannot be opened.\n");
}
return 0;
}
例6-17 UVA 10562 看图写数 (p170, 多叉数 DFS)
思路
其实就是递归遍历树,递归多写几遍掌握了这种题就很容易。另外根本不需要建树,直接递归输出就行。
WA了一次,认真看了一下原题发现还需要处理空树的情况,确实漏掉了。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 200;
int n;
char dat[N+1][N+1];
void prase(int x, int y0, int y1)
{
for (int y = y0; y <= y1; y ++) {
if (y >= 0 && y < strlen(dat[x]) && dat[x][y] != ' ') {
printf("%c(", dat[x][y]);
if (x < n-1 && dat[x+1][y] == '|') {
int m0 = y, m1 = y;
while (m0 >= 0 && dat[x+2][m0] == '-') m0 --;
while (dat[x+2][m1] == '-') m1 ++;
prase(x+3, m0+1, m1-1);
}
printf(")");
}
}
}
int main(void)
{
int t;
cin >> t;
getchar();
while (t --) {
n = 0;
while (gets(dat[n]) && strcmp(dat[n], "#"))
n ++;
printf("(");
if (n) prase(0, 0, strlen(dat[0])-1);
printf(")\n");
}
return 0;
}
例6-18 UVA 12171 雕塑 (p171, 离散化: floodfill)(未尝试)
思路
代码
例6-19 UVA 1572 自组合 (p172, 图论模型)
思路
待补充。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 52;
bool G[N][N];
int c[N];
void init()
{
memset(G, 0, sizeof(G));
memset(c, 0, sizeof(c));
}
bool dfs(int u)
{
c[u] = -1;
for (int k = 0; k < N; k++) {
if (G[u][k]) {
if (c[k] < 0) return false; //存在有向环
else if (!c[k] && !dfs(k)) return false;
}
}
c[u] = 1;
return true;
}
bool toposort()
{
for (int u = 0; u < N; u++) {
if (!c[u]) {
if (!dfs(u)) return false;
}
}
return true;
}
int main(void)
{
int m;
while (scanf("%d", &m) != EOF) {
init();
char s[10];
while (m--) {
scanf("%s", s);
int a[4];
for (int i = 0; i < 4; i++) {
a[i] = N;
if (isalpha(s[2*i])) {
a[i] = s[2*i]-'A';
if (s[2*i+1] == '+') a[i] += 26;
}
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (i != j && a[i] < N && a[j] < N) {
G[a[i]][(a[j]+26)%N] = 1;
//printf("%d %d\n", a[i], a[j]);
}
}
}
}
if (toposort()) printf("bounded\n");
else printf("unbounded\n");
}
return 0;
}
例6-20 UVA 1599 理想路径 (p173, 图的 BFS 树)(未尝试)
思路
代码
例6-21 UVA 506 系统依赖 (p173, 图的概念与拓扑排序)(未尝试)
思路
代码
例6-22 UVA 11853 战场 (p175, 对偶图)(未尝试)
思路
代码