UVa 12107 Digit Puzzle 题解

Posted alrond

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UVa 12107 Digit Puzzle 题解相关的知识,希望对你有一定的参考价值。

难度:β

建议用时:45 min

实际用时:3 h 30 min

??(你看不出来这是题目链接,对吧?(手动滑稽))

 

这是我目前为止独立完成的最复杂的一道题。(别喷我太水)

这样一道暴力搜索的题,怎么会花如此多时间?

因为我一直在改细节。

调试了很多。大多数时间都在调试。需要考虑的细节真的蛮多的。

 

下面分析算法。

 

这题我看有大神用数组 AC。然而本人因为水平有限,决定用字符串。于是时间肯定要爆。不过我有优化方法,过一会再说。

虽然用了优化,也只是 2500 ms 勉强过。??

 

题目要求以最少的改动数使得方程只有唯一解。其中两个乘数最多只有 2 位,所以乘出来的数最多有 4 位。

每一位上的数要么是符号 “*”,代表这里可以填任意数,要么是 “0123456789” 中的一个。

于是每一位数只有 11 种可能性。整个方程也只有 11 ^ 8 种可能的排列。

 

在看主要算法之前,先考虑一下怎样判断一个已知的字符方程是否有且只有唯一解。

为了表示方便,我把两个乘数各自统一为 2 个字符,积统一为 4 个字符。缺的字符用 “#” 代替。

注意不能用 “0”!因为我们要求三个数的长度是一定的,对于超出限制的高位数需要用 “#” 加以区分。

 1 string standarlize(string x, int size) {
 2     string sx = "##";
 3     if (size == 2) {
 4         if (x.length() == 2) sx = x;
 5         if (x.length() == 1) sx[1] = x[0];
 6     } // 2 -> #2
 7     
 8     if (size == 4) {
 9         sx += "##";
10         if (x.length() == 1) sx[3] = x[0];
11         if (x.length() == 2) { sx[3] = x[1]; sx[2] = x[0]; }
12         if (x.length() == 3) { sx[3] = x[2]; sx[2] = x[1]; sx[1] = x[0]; }
13         if (x.length() == 4) sx = x;
14     } // 312 -> #312 
15     
16     return sx;
17 }

然后为了待会儿枚举方便,我把三个数整合成一个 8 位字符数。

xyz += stx; xyz += sty; xyz += stz;

待会用时,可以直接把 xyz 拆开。

好吧。就说我们手上有了这样三个字符数。

“*1” * “#2” = “##**” (注意 “#”  是占位符,不参与计算,数字位数也不能超过这个符号的限制)

这个方程有几个解?

你肯定会脱口而出:4 个!(11 * 2 = 22, 21 * 2 == 42, 31 * 2 == 62, 41 * 2 == 82)

那么你是怎样的出这个答案的呢?

“*1” 里的 “*” 可以取 “1”,“2”,“3”,“4”。

还是用的简单枚举法,对吧?

那么我们就可以用这种方法来球解的个数了。

算法是:枚举前两个乘数,得到一个积。判断两个乘数和积是不是和字符数相匹配。(匹配指诸如 “*2” = “12”,“#2” != “12”,“1234” = “1234” 这类)

(这里省略代码)(~~)

 

好的。现在知道怎样判断一个给定的方程有没有唯一解了。下面就是看怎样枚举方程了。

方程既然用 8 位字符串表示好了,那么就用 dfs 一个一个依次从左到右试着改变原方程的某一位,从而得到一个不同的方程。

注意:这里枚举要用迭代加深,否则会超时。

 1 bool dfs(int d, int cur) {
 2     if (d == maxd) {
 3         if (one_and_only_solution())
 4             return true;
 5         else
 6             return false;
 7     }
 8     
 9     if (cur == 8) return false;
10     
11     string cpy = xyz; // 高亮这里
12     for (int i = 0; i <= 10; i++) {
13         if (xyz[cur] == patt[i] || xyz[cur] == #) {
14             if (dfs(d, cur+1)) return true;
15         } else {
16             xyz[cur] = patt[i];
17             if (dfs(d+1, cur+1)) return true;
18         }
19         xyz = cpy; // 这里也是
20     }
21     
22     return false;
23 }

留心一下 cpy 的作用。否则整个枚举会一团糟。

这题的一个麻烦就在于它给的条件比较多。要搞清楚 “0123456789” “#” “*” 各自的作用才可以把 if 写正确。

 

下面就只剩下判断是否唯一解了。

这里我开始在枚举乘数的时候是用 0 ~ 99,然而这样花了很多不必要的时间(比如你手上有一个“#1”, 然而你给它枚举了 100 次,每次还判断了是不是匹配的)

如果这里枚举不用优化的话,分分钟超时。(我测过的)

我的想法是在程序执行前就提前建好每一种 2 位字符数匹配的 2 位数字。(注意是“字符串”对应“数字集合”, 自然想到用 map 对应 vector,vector 比 set 方便枚举)

 1 void create_data() {
 2     lib.clear();
 3     string base = "  ";
 4     for (int i = 0; i <= 11; i++)
 5         for (int j = 0; j <= 11; j++) {
 6             base[0] = patt[i]; base[1] = patt[j]; // patt = "*0123456789"
 7             for (int k = 0; k <= 99; k++) {
 8                 if (match(base, k)) {
 9                     lib[base].push_back(k);
10                 }
11             }
12         }
13 }

这样枚举时效率快多了,可以卡进 3 s 时限了。

 

下面是判断函数。

 1 bool one_and_only_solution() {
 2     string xx = "", yy = "", zz = "";
 3     xx += xyz[0]; xx += xyz[1];
 4     yy += xyz[2]; yy += xyz[3];
 5     zz += xyz[4]; zz += xyz[5];
 6     zz += xyz[6]; zz += xyz[7];
 7      
 8     int cnt = 0;
 9     for (int i = 0; i < (int)lib[xx].size(); i++) { 
10         for (int j = 0; j <(int)lib[yy].size(); j++) {
11             int v1 = lib[xx][i], v2 = lib[yy][j];
12             int v3 = v1 * v2;
13             if (!compare(zz, v3)) continue;
14             cnt++;
15             if (cnt >= 2) return false; // 必要的优化
16         }
17     }
18     
19     return cnt == 1;
20 }

咦,这中间有一个 compare 函数。这是什么?

聪明的你一定能发现,这个函数是用来比较积是否匹配的。

我原本打算把积拆成两半,用两个 2 位数在 vector 里找。然而细思过后觉得不行,于是用下面的笨方法。

 1 bool compare(string cmp, int val) {
 2     if (val == 0) {
 3         if ((cmp[3] == 0|| cmp[3] == *) && cmp[2] == #) return true;
 4         return false;
 5     }
 6     string cmp2 = "####";
 7     int iter = 3;
 8     while (val) {
 9         cmp2[iter--] = char(val % 10 + 0);
10         val /= 10;
11     }
12     for (int i = 0; i <= 3; i++) {
13         if (cmp[i] == * && cmp2[i] != #) continue;
14         if (cmp2[i] != cmp[i]) return false;
15     }
16     return true;
17 }

现在所有的都搞定了。

 

提交爆出 2500 ms。虽然 uDebug 上给出了很强的数据,但是评测时显然放松了许多(大概是考虑到我这种弱渣的心情吧)。

 

2018-01-25

以上是关于UVa 12107 Digit Puzzle 题解的主要内容,如果未能解决你的问题,请参考以下文章

UVa-12107 Digit Puzzle

题解Puzzle [Uva1399]

UVa 1604 Eight Cubic-Puzzle 题解

例题3-5 Digit Generator UVA - 1583

UVa 1604 Eight Cubic-Puzzle 优化方案

习题 7-9 UVA-1604Cubic Eight-Puzzle