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 題解