https://vjudge.net/status#un=&OJId=UVA&probNum=11214&res=0&orderBy=run_id&language=
https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2155
这道题目是经典的八皇后问题八皇后问题的一个衍生。
虽然问题和原八皇后问题有点出入,但是解题思路基本上还是一致的。
分析
首先,根据例题给的数据当8X8格子填满的时候的解是5。这里我们已经可以看到解的数量是远远小于整个图的状态(8X8 = 64)的。根据这个我们就已经能狗很好的推断这道题选用迭代加深(ID)算法会是一个很好的选择,因为长和宽的上限不超过9。
算法速度分析
这个是迭代加深的一个模版伪代码
bool bt(int d, int maxd) {
if (maxd == d) {
if (检查有解()) return true;
return false;
}
循环所有状态 {
保存当前状态();
改变当前状态到可能的下一次状态();
if(bt(d + 1, maxd)) return true;
改回当前状态();
}
return false;
}
int main() {
for (int maxd = 0;; maxd++) {
if (bt(0, maxd, 0, 0)) {
打印答案();
break;
}
}
return 0;
}
从中我们能看出能够影响迭代加深的速度的几个因素:
- 循环所有状态的次数
- 改变状态需要消耗的速度
- 检查解的速度,这个速度相对比2)影响要小一些,因为只用考虑(d == maxd)的情况。
循环次数
首先,不管给什么样的grid,假设这道题grid的高是n,宽m,解是x,所有可能解的状态就等于在m*n的格子里选择x个格子放皇后。直接套用组合数公式就能算出总共需要
C
x
m
∗
n
=
(
m
∗
n
)
!
(
m
∗
n
−
x
)
!
x
!
C_x^{m*n} = \frac {(m*n)!} {(m*n- x)!x!}
Cxm∗n=(m∗n−x)!x!(m∗n)!
次循环。
根据给的限制条件,我们知道最大可能的情况是9X9的格子里全部被mark了,因为已知8X8的解是5,很容易就能推出9X9的解不是5就是6(最终算出来发现是5)。
我们先假设是6,根据上边推出的公式算出最大可能循环次数
81
∗
80
∗
79
∗
78
∗
77
∗
76
6
∗
5
∗
4
∗
3
∗
2
∗
1
=
324540216
\frac {81*80*79*78*77*76} {6*5*4*3*2*1} =324540216
6∗5∗4∗3∗2∗181∗80∗79∗78∗77∗76=324540216
次。
但是实际上因为我们找到解就返回了,而在有6个皇后的情况下,第一个皇后的位置肯定在第一行第一个(这个是根据8X8有五个解的情况推出来的)这样的话,其实最慢速度就是完全循环完5解
81
∗
80
∗
79
∗
78
∗
77
5
∗
4
∗
3
∗
2
∗
1
=
25621596
\frac {81*80*79*78*77} {5*4*3*2*1} =25621596
5∗4∗3∗2∗181∗80∗79∗78∗77=25621596次。这个循环次数对于给定的限时6666ms来说已经非常能够接受了。而真正的跑的时候的速度其实比这个还要快大概5-10倍,因为最大解只有5次,而第一个皇后很有可能在第一行。所以一个皇后放在第一行就会停止了。
改变状态和检查解的速度
如果我们只考虑最简单的算法:每次放皇后都清除掉放在路径上的X,检查的时候看这个grid是不是空的。这两个都需要完全循环整个grid,也就是说,最慢的速度就是长乘以宽9X9=81次。基于上边分析,不负责任的推测这个速度可能是可以跑完这个题的。(笔者并没有试过)
优化
改变状态和检查解的速度优化
这道题的优化思路来源于八皇后问题。每次放皇后我们并不需要改变grid的状态,而是可以直接标记皇后有经过当前点的行,列,对角线和反对角线。这样的话改变状态的时间效率直接从O(mxn)减少到了O(4)。而检查解的时候我们只用检查是不是每个X的位置上有没有任意的行,列,对角线和反对角线被标记过,如果没有直接返回false。这样虽然还是要循环mxn次,但是状态还是有一定的减少。
循环次数优化
如果所在的行,列,对角线和反对角线都被标记过,当前位置可以不用标记,这个条件我感觉加上会有一定的效果,不过效果有限。
耍赖皮的优化方法
如果为了提高速度,因为已知最大的答案是5,可以只用循环到4,如果没找到解直接输出结果5。这样最后最慢的那次循环可以不用做。
AC代码
这个是不耍赖皮的版本。如果想要耍赖皮,请读者自己实验
#include<bits/stdc++.h>
using namespace std;
int n, m;
bool grid[10][10], r[10], c[10], di[20], rd[20];
bool bt(int d, int maxd, int x, int y) {
if (maxd == d) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] && !r[i] && !c[j] && !di[i + j] && !rd[i - j + m - 1]) return false;
}
}
return true;
}
for (int i = x; i < n; i++) {
for (int j = ((i == x) ? y : 0); j < m; j++) {
bool cr = r[i], cc = c[j], cd = di[i + j], crd = rd[i - j + m - 1];
if (cr && cc && cd && crd) continue; // all marked, just go next
r[i] = c[j] = di[i + j] = rd[i - j + m - 1] = 1;
int ni = i, nj = j + 1;
if (nj == m) nj = 0, ni++;
if(bt(d + 1, maxd, ni, nj)) return true;
r[i] = cr, c[j] = cc, di[i + j] = cd, rd[i - j + m - 1] = crd;
}
}
return false;
}
int main() {
int kase = 1;
while(cin >> n >> m && n && m) {
getchar();
for (int i = 0; i < n ;i++) {
for (int j = 0; j < m; j++) grid[i][j] = (getchar() == 'X');
getchar();
}
for (int maxd = 0;; maxd++) {
memset(r, 0, sizeof(r));
memset(c, 0, sizeof(c));
memset(di, 0, sizeof(di));
memset(rd, 0, sizeof(rd));
if (bt(0, maxd, 0, 0)) {
printf("Case %d: %d\n", kase++, maxd);
break;
}
}
}
return 0;
}
小结
这题比起我前几遍博客里边的题,这道题算比较简单了。这道题的关键在于算法的速度分析,只要能够推算出大概速度,最后写出的解也是比较有底气的。对于优化方法也是完全是借鉴了八皇后问题 ,只要把那道经典题掌握了,想出解来并不难。