題目二 相似字符串
時間限制: 4000ms 內存限制: 256MB
描述
對於兩個長度相等的字符串,我們定義其距離為對應位置不同的字符數量,同時我們認為距離越近的字符串越相似。例如,“0123”和“0000”的距離為 3,“0123”和“0213”的距離則為 2,所以與“0000”相比,“0213”和“0123”最相似。
現在給定兩個字符串 S1 和 S2,其中 S2 的長度不大於 S1。請在 S1 中尋找一個與 S2 長度相同的子串,使得距離最小。
輸入
輸入包括多組數據。第一行是整數 T,表示有多少組測試數據。每組測試數據恰好占兩行,第一行為字符串 S1,第二行為 S2。所有字符串都只包括“0”到“9”的字符。
輸出
對於每組測試數據,單獨輸出一行“Case #c: d”。其中,c 表示測試數據的編號(從 1 開始),d 表示找到的子串的最小距離。
數據范圍
1 ≤ T ≤ 100
小數據:字符串長度不超過 1000
大數據:字符串長度不超過 50000
樣例輸入
3
0123456789
321
010203040506070809
404
20121221
211
樣例輸出
Case #1: 2
Case #2: 1
Case #3: 1
解題思路
這道題其實不復雜,結果我很蛋疼的用了一個字符串近似匹配的 DP 算法,結果果然悲劇了……在這里就只好讀讀大神們的代碼,看看有什么給力的解法。
首先設兩個字符串分別為 s1 和 s2,它們的長度為 $m$ 和 $n$,其中 $m \ge n$。
最簡單的方法是直接暴力字符串匹配,就是嘗試將每個 $s{1_{i \ldots i + n}}$ 和 $s2_{0 \ldots n}$ 進行匹配(排名第二的 Tripod2K 就是這么過的),不過需要注意如果距離已經大於當前的最小距離了,就不必繼續匹配下去了,否則是會悲劇的。在比賽結束之后可能又更新測試數據后 Rejudge 了,所以 Tripod2K 的算法已經被判為超時,所以此算法僅供參考。其核心算法為:
int min = n; // 初始最小距離為 n for(int i = 0; i <= m-n; i++) { int dis= 0; // 嘗試匹配 s1[i...i+n] 和 s2 for(int j = 0; j < n; j++) { if(s1[i + j] != s2[j]) dis++; if(dis >= min) { // 這里 dis 已經超過 min 了,再繼續匹配下去也沒有什么用 break; } } if(dis < min) min = dis; if(min == 0) { // min 到達了 0,已經不可能更小了,也不必再匹配下去了 break; } }
完整的代碼為:(鏈接在這里)
import java.util.Scanner; public class Main { public static void main(String args[]){ Scanner in = new Scanner(System.in); int t = in.nextInt(); byte[] s1 = new byte[60000]; byte[] s2 = new byte[60000]; for(int i = 1; i <= t; i++){ s1 = in.next().getBytes(); s2 = in.next().getBytes(); int d = distance(s1, s2); System.out.println("Case #" + i + ": " + d); } } private static int distance(byte[] s1, final byte[] s2){ int l1 = s1.length; int l2 = s2.length; int result = l2; for(int i = 0; i <= l1 - l2; ++i){ int tmp= 0; for(int j = 0; j < l2; ++j){ if(s1[i + j] != s2[j]) tmp++; if(tmp >= result) break; } if(tmp < result) result = tmp; if(tmp == 0) break; } return result; } }
還有排名第四的 chaozicen 的算法,則比較巧妙。現在假設 s1="010203040506070809",s2="404",首先對 s1 進行預處理,標記出每個字符出現的索引,得到下面的表格:
字符 | 出現的索引 |
'0' | 0,2,4,6,8,10,12,14,16 |
'1' | 1 |
'2' | 3 |
'3' | 5 |
'4' | 7 |
'5' | 9 |
'6' | 11 |
'7' | 13 |
'8' | 15 |
'9' | 17 |
然后在讀取 s2 的每個字符時,根據表格在一個長為 $n$ 的 int 數組 ans 上進行標記 s2[0] 出現的位置。例如現在 s2[0] = '4',表格中字符 '4' 對應的索引只有 7,那么令 ans[7-0]++;然后讀取 s2[1]='0',表格中字符 '0' 對應的索引有 0,2,4,6,8,10,12,14 和 16,那么就令 ans[0-1]、ans[2-1]、ans[4-1]、ans[6-1]、ans[8-1]、ans[10-1]、ans[12-1]、ans[14-1] 和 ans[16-1] 均加一;最后讀取 s2[2] = '4',令 ans[7-2]++。最后得到的 ans 數組如下所示:
ans = {0, 1, 0, 1, 0, 2, 0, 2, 0, 1, 0, 1, 0 ,1, 0, 1};
那么這個 ans 表示什么呢?ans[i] 表示 $s1_{i \ldots i+n}$ 與 $s2_{0 \ldots n}$ 中完全相同的字符的個數,最后只要統計 ans 中最大的那個,就表示距離最小。
下面是 chaozicen 的源代碼,鏈接在這里:
#include<iostream> #include<string> using namespace std; int num[10][60000],ans[60000]; int main() { int T,n,i,k,j,s,l1,l2,anss,t; string s1,s2; cin>>T; t=1; while (T>=t) { ++t; cin>>s1;cin>>s2; l1=s1.length(); l2=s2.length(); for (i=0;i<10;++i) num[i][0]=0; for (i=0;i<l1;++i) { k=s1[i]-'0'; ++num[k][0]; num[k][num[k][0]]=i; ans[i]=0; } for (i=0;i<l2;++i) { k=s2[i]-'0'; for (j=1;j<=num[k][0];++j) { if (num[k][j]-i>=0) ans[num[k][j]-i]++; } } anss=0; for (i=0;i<l1-l2+1;++i) if (ans[i]>anss) anss=ans[i]; anss=l2-anss; cout<<"Case #"<<t-1<<": "<<anss<<endl; } return 0; }
題目三是一個好復雜的搜索,我根本沒仔細看,題目一我也沒搞明白怎么個是最優策略。