1. 程式人生 > >UVa 12107 Digit Puzzle 題解

UVa 12107 Digit Puzzle 題解

判斷函數 超時 之前 改變 搜索 鏈接 你是 on() 一位

難度:β

建議用時: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 題解