难度:β
建议用时: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