1. 程式人生 > 實用技巧 >[NOIp提高組]子串「DP」

[NOIp提高組]子串「DP」

[NOIp提高組]子串「DP」

題目描述

有兩個僅包含小寫英文字母的字串 \(A\)\(B\)

現在要從字串 \(A\) 中取出 \(k\) 個互不重疊的非空子串,然後把這 \(k\) 個子串按照其在字串 \(A\) 中出現的順序依次連線起來得到一個新的字串。請問有多少種方案可以使得這個新串與字串 \(B\) 相等?

注意:子串取出的位置不同也認為是不同的方案。

輸入格式

第一行是三個正整數 \(n,m,k\),分別表示字串 \(A\) 的長度,字串 \(B\) 的長度,以及問題描述中所提到的 \(k\),每兩個整數之間用一個空格隔開。

第二行包含一個長度為 \(n\) 的字串,表示字串 \(A\)

第三行包含一個長度為 \(m\) 的字串,表示字串 \(B\)

輸出格式

一個整數,表示所求方案數。

由於答案可能很大,所以這裡要求輸出答案對 \(1000000007\) 取模的結果。

輸入輸出樣例

輸入 #1

6 3 1 
aabaab 
aab

輸出 #1

2

說明/提示

對於所有 \(10\) 組資料:\(1≤n≤1000,1≤m≤200,1≤k≤m\)


思路分析

ps:這題常規方法不難,關鍵說一下優化(因為時間和空間都很容易炸)

  • 這種給兩個資訊進行互相匹配的\(DP\)的轉移狀態還是比較常規的,陣列記錄一下兩個字串當前各自的結尾即可,另外這題要再開一維記錄有多少串,所以很容易想到用陣列\(f[i][j][k]\)
    來轉移,\(i\)\(A\)串的結尾,\(j\)\(B\)串的結尾,\(k\)為所選串的個數
  • 轉移方程根據匹配情況來推即可,另外一開始理解錯了題目,以為選的子串是不能連著的,結果發現是可以的,你完全可以把一個子串拆成若干個小子串……
    • 首先對於 \(A[i]!=B[j]\) 的情況,很簡單,就是 \(f[i-1][k]\),因為沒有做貢獻
    • 對於 \(A[i]=B[j]\) 的情況,我們就完全可以增加一串,上面說了,這幾個子串只是不能重疊,但可以連著,所以我們只需從前面 \(k-1\) 串裡往前挨個轉移,直到一個 \(A[i-x]!=B[j-x]\) 的為止,因為相等的都可以和 \(i\)\(j\)
      連起來作為一組,當然也可以全部不選
  • 轉移方程:
    • \(f[i][j][k] = f[i-1][j][k],A[i]!=B[j]\)
    • \(f[i][j][k] = f[i-1][j][k] + \sum_{x=1}^{t}f[i-x][j-x][k-1]\),\(A[t]!=B[t]且A(t,i]=B(t,j]\) (左開右閉)
  • 三維空間太大,而且這樣轉移時間效率也很低,怎麼壓維和優化時間?
  • 我們肯定要優先考慮將 \(i\) 這一維壓掉,但是本蒟蒻想了半天也想不出怎麼樣,於是看了一下題解整個傻掉
  • \(i\) 不好壓掉的原因在於相同的 \(j\)\(k\) 在不同的i下有不同的值,如果還是像上面那個轉移方程一樣把區間內符合條件的挨個轉移肯定是不行的,所以我們考慮整體轉移,使用字首和思想

Code

#include<cmath>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define int long long //一年OI一場空,不開long long見祖宗
#define N 1010
#define M 210
using namespace std;
inline int read(){
	int x = 0,f = 1;
	char ch = getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int mod = 1000000007;
int n,m,K,f[M][M],sum[M][M];
char a[N],b[M];
signed main(){
	n = read(),m = read(),K = read();
	scanf("%s%s",a+1,b+1);
	f[1][0] = 1; //最初對應一種方案
	for(int i = 2;i <= n+1;i++){ //2~n+1,其實是將轉移滯後到了它的下一個,因為字串是從1到n
		for(int j = m+1;j >= 2;j--){ //一定要用倒序,這樣你的sum陣列才是從前面的i轉移過來的
			for(int k = K;k >= 1;k--){
				sum[j][k] = a[i-1]==b[j-1] ? sum[j-1][k]+f[j-1][k-1] : 0;  //即用到了字首和思想,又用到了動態規劃思想,實在是妙(就這兩行我想了一晚上)
				f[j][k] = (f[j][k]+sum[j][k])%mod; //因為壓掉了一維,所以直接加上sum即可
			}
		}
	}
	printf("%lld\n",f[m+1][K]);
	return 0;
}

DP就是這麼短小精悍的東西