1. 程式人生 > 其它 >簡單普通 DP

簡單普通 DP

too young too simple

*I. P3643 [APIO2016]划艇

題意簡述:給出序列 \(a_i,b_i\),求出有多少序列 \(c_i\) 滿足 \(c_i=-1\)\(c_i\in[a_i,b_i]\),同時非 \(-1\) 的部分單調遞增。

直到做到這題我才意識到我的 DP 水平有多菜。

注意到值域過大,於是對區間進行離散化,設離散化後的端點分別為 \(p_1,p_2,\cdots,p_c\)。注意要將 \([a_i,b_i]\) 變成 \([a_i,b_i+1)\),即\(b_i\)\(1\),方便計算答案。

接下來考慮 DP:設 \(f_{i,j}\) 表示第 \(i\) 個學校派出划艇數量在 \(L_j=[p_j,p_{j+1})\)

之間時的方案數。

錯誤思路:\(f_{i,j}=\begin{cases}\sum_{pos=1}^{i-1}\sum_{k=1}^{j-1}f_{pos,k}\times (p_{j+1}-p_j)&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)。錯誤原因:I. 沒有考慮邊界條件 & 列舉下界。II. 沒有考慮在同一區間內也合法的情況。

邊界條件就是 \(f_{i,0}=1\),並且注意 \(pos,k\) 的列舉下界應為 \(0\)

考慮在同一區間內合法的情況:不妨列舉最右端的不在區間 \(j\)

的位置 \(pos\),那麼剩下來 \(i-pos\) 個位置。假設當中有 \(m\) 個位置滿足可以落在區間 \(j\) 上,那麼方案數就相當於從 \(m-1\)\(-1\)\(L_j\) 個數 \(p_j,p_j+1,\cdots,p_{j+1}-1\) 中選出 \(m\) 個數(因為位置 \(i\) 必須選所以是 \(m-1\) 而不是 \(m\)\(-1\) 相當於不選),即 \(\binom{m+L_j-1}{m}\)

綜上所述,更新後的轉移方程應為 \(f_{i,j}=\begin{cases}\sum_{pos=0}^{i-1}\sum_{k=0}^{j-1}f_{pos,k}\times\binom{m+L_j-1}{m}&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)

。注意到後面的組合數可以 \(\mathcal{O}(1)\) 遞推(\(\binom{m+L_j}{m+1}=\binom{m+L_j-1}{m}\times\frac{m+L_j}{m+1}\)),那麼使用字首和優化(因為 \(m\) 與列舉的 \(k\) 無關,所以記 \(s_{i,j}=\sum_{k=0}^j f_{i,k}\),則上面那一坨可以寫成 \(\sum_{pos=0}^{i-1}s_{pos,j-1}\times\binom{m+L_j-1}{m}\))+ 倒序列舉 \(pos\)(實時更新 \(m\) 與組合數)即可 \(\mathcal{O}(n^3)\) 計算。

#include <bits/stdc++.h>
using namespace std;

#define ll long long

const int N=500+5;
const int mod=1e9+7;

ll n,cnt,a[N],b[N],p[N<<1];
ll g[N],iv[N];

int main(){
	cin>>n,g[0]=1;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i],iv[i]=(i==1?1:-mod/i*iv[mod%i]%mod+mod);
		p[++cnt]=a[i],p[++cnt]=b[i]+1;
	} sort(p+1,p+cnt+1),cnt=unique(p+1,p+cnt+1)-p-1;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(p+1,p+cnt+1,a[i])-p;
		b[i]=lower_bound(p+1,p+cnt+1,b[i]+1)-p;
	} for(int i=1;i<cnt;i++){
		ll len=p[i+1]-p[i];
		for(int j=n;j;j--)
			if(a[j]<=i&&i<b[j]){
				ll f=0,m=0,C=1;
				for(int k=j;k;k--){
					if(a[k]<=i&&i<b[k])C=C*(m+len)%mod*iv[m+1]%mod,m++;
					f=(f+g[k-1]*C)%mod;
				} g[j]=(g[j]+f)%mod;
			}
	} ll ans=0;
	for(int i=1;i<=n;i++)ans=(ans+g[i])%mod;
	cout<<ans<<endl;
	return 0;
}

II. P7091 數上的樹

題目傳送門

首先將 \(n\) 分解質因數,用 DFS 求出 \(n\) 的所有因數,記為 \(d_1,d_2,\cdots,d_c\),跑一遍反素數那題的程式碼可知 \(c\leq 23327\)

\(f_i\) 表示根節點為 \(d_i\) 時最小值。

顯然,區域性最優值可以保證整體最優值,且轉移無後效性,即求 \(f_i\) 時不會影響 \(f_j\ (d_j<d_i)\),故答案可以用樹形 DP 求出,將所有因數排序後可以轉化為序列上的 DP。

對於不能出現在樹上的 \(d_i\) 直接 skip 即可。

\(g_i\) 表示 \(d_i\) 所含有的質因子個數。For example,\(12=2\times 2\times 3\),所以 \(12\)\(3\) 個質因子。在本題中,\(g_{i}\) 也表示以 \(d_i\) 為根的子樹的節點個數,不難發現其為定值。

假設當前轉移 \(f_i\) 決策點為 \(j,k\ (d_j\times d_k=d_i)\),那麼對於 \(d_j\)\(d_k\) 子樹內兩兩組合出的 pairs 的貢獻可以直接由 \(f_j+f_k\) 推得,剩下來只有兩種情況:

  • Case 1:\(d_j\)\(d_k\) 子樹內各一個節點組合出的 pairs。因為它們的 LCA 是 \(d_i\),且共有 \(g_j\times g_k\) 對 pairs,故貢獻為 \(g_j\times g_k\times d_i\)
  • Case 2:\(d_i\) 和任意一個節點組合出的 pairs。顯然貢獻為 \(g_i\times d_i\)

轉移方程:

\[f_i=\min_{d_j\times d_k=d_i}f_j+f_k+(g_j\times g_k+g_i)\times d_i \]

,其中 \(g_i=g_j+g_k+1\) 可以在 DP 時一併求出。

這樣子搞是 \(\mathcal O(c^3)\) 的,顯然無法接受。

  • 剪枝 1:在列舉內層迴圈 \(j\) 時發現 \(k\) 有單調性,所以直接用 pointer 代替 \(k\) 即可。這樣時間複雜度降為了 \(\mathcal O(c^2)\)
  • 剪枝 2:當 \(j>k\) 時直接 break,減小常數。

綜上,我們有了一個 \({\mathcal O}(\sqrt n+m\log c+c^2)\) 的演算法(分解質因數 + 處理限制需要二分查詢 + DP),程式碼如下:

ll n,m,num[N],f[N];
ll cnt,pr[N],c[N],tot;
ll fc[N],il[N],d;
map <ll,int> isp;

void dfs(int pos,ll prod){
	if(pos>cnt){
		if(prod>1)fc[++d]=prod;
		return;
	} for(int i=0;i<=c[pos];i++)dfs(pos+1,prod),prod*=pr[pos];
}

int main(){
	
	cin>>n>>m;
	// factor
	ll tmp=n;
	for(ll i=2;i*i<=n;i++)
		if(n%i==0){
			pr[++cnt]=i,isp[i]=1;
			while(n%i==0)c[cnt]++,tot++,n/=i;
		}
	if(n>1)pr[++cnt]=n,tot++,c[cnt]=1,isp[n]=1;
	n=tmp;
	
	// find factors
	dfs(1,1);
	sort(fc+1,fc+d+1);
	
	// limit
	for(int i=1;i<=m;i++){
		ll val=read();
		int pos=lower_bound(fc+1,fc+d+1,val)-fc;
		if(pos<=d&&fc[pos]==val)il[pos]=1; // 表示 pos 不能出現
	}
	
	// dp
	for(int i=1;i<=d;i++){
		if(il[i])continue;
		if(isp[fc[i]]){
			num[i]=1,f[i]=fc[i];
			continue;
		} il[i]=1,f[i]=inf; // 如果無法由以前的 j,k 轉移得到那麼 i 也無法得到
		int p=i-1;
		for(int j=1;j<i;j++){
			if(fc[i]%fc[j])continue;
			while(fc[p]>fc[i]/fc[j])p--;
			if(j>p)break;
			if(!il[j]&&!il[p])f[i]=min(f[i],f[j]+f[p]+num[j]*num[p]*fc[i]+(num[i]=num[j]+num[p]+1)*fc[i]),il[i]=0;
		}
	} if(il[d])puts("-1");
	else cout<<(ll)f[d]<<endl;
	return 0;
}

III. CF1156F Card Bag

題目傳送門

題意簡述:有 \(n\) 張卡牌,每張卡牌有數字 \(a_1,a_2,\cdots,a_n\)。現在隨機抽取卡牌,不放回,設本次抽到的卡牌為 \(x\),上次抽到的卡牌為 \(y\),若 \(x=y\) 則遊戲勝利;若 \(x<y\) 則輸掉遊戲;若 \(x>y\) 則遊戲繼續。求獲勝概率。

\(a_i\leq n\leq 5\times 10^3\)

下文認為 \(a_i\)\(n\) 同階。

不難發現我們只關心卡牌上的數字,所以開個桶維護每個數出現了幾次。又因為只能從小往大抽,即無後效性,所以考慮 DP。

\(f_{i,j}\)共抽了 \(j\) 次,每個數最多抽到一次,最後一次抽到數字 \(i\) 的概率。

首先考慮如何轉移:我們設數字 \(i\) 共有 \(sz_i\) 個,那麼不難列出轉移方程

\[f_{i,j}=\sum_{k=0}^{i-1}f_{k,j-1}\times \frac{sz_i}{n-j+1} \]

,表示 \([1,i-1]\) 中抽了 \(j-1\) 個數 的概率乘上 抽到數字 \(i\) 的概率。這樣轉移的時間複雜度為 \(\mathcal{O}(n^3)\),無法接受。

如果設 \(s_{i,j}\)\(i\) 中抽了 \(j\) 個數 的概率,則有

\[s_{i,j}=\sum_{k=1}^{i}f_{i,j} \]

,則轉移方程可變形為

\[f_{i,j}=\frac{s_{i-1,j-1}sz_i}{n-j+1} \]

。預處理逆元做到時間複雜度 \(\mathcal{O}(n^2)\),可以接受。

這實際上就是具有實際意義的字首和優化。

最後使用滾動陣列可以將空間優化到 \(\mathcal{O}(n)\)

需要注意初始值 \(f_{0,0}=1\)

const int N=5e3+5;
ll n,ans,sz[N],f[2][N],s[2][N];
int main(){
	init(),cin>>n,s[0][0]=s[1][0]=1;
	for(int i=1,a;i<=n;i++)cin>>a,sz[a]++;
	for(int i=1,p=1;i<=n;i++,p^=1){
		for(int j=1;j<=i;j++){
			f[p][j]=s[p^1][j-1]*sz[i]%mod*iv[n-j+1]%mod;
			ans=(ans+s[p^1][j-1]*sz[i]*(sz[i]-1)%mod*iv[n-j+1]%mod*iv[n-j])%mod;
			s[p][j]=(s[p^1][j]+f[p][j])%mod;
		}
	} cout<<ans<<endl;
	return 0;
}

*IV. CF1542E2 Abnormal Permutation Pairs (hard version)

題解

*V. AT693 文字列

很 nb 的題目,沒想出來。

注意我們不關注字元的相對順序,只關心有沒有相鄰的相同字元,因此考慮 DP:設 \(f_{i,j}\) 表示前 \(i\) 個字母構成的有 \(j\) 對相鄰字元的字串個數。轉移時列舉 \(j\),當前字母分幾段,以及一共插入到幾對相鄰字元中,然後組合數算算即可。答案即為 \(f_{n,0}\)

時間複雜度 \(\alpha n^3\),其中 \(\alpha\) 是字符集大小,\(n\) 是字元總數。

可是我連第一步 DP 都沒想出來。

#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define pb push_back
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))

const ll mod=1e9+7;
const int N=260+5;

ll c,sum,a[N],f[N][N],C[N][N];
int main(){
	for(int i=0;i<N;i++)
		for(int j=0;j<=i;j++)
			C[i][j]=(j==0||j==i?1:C[i-1][j-1]+C[i-1][j])%mod;
	for(int i=0;i<26;i++)cin>>a[i];
	f[0][max(0ll,a[0]-1)]=1,sum=a[0];
	for(int i=1;i<26;i++){
		if(!a[i])continue;
		c++;
		for(int j=0;j<=sum;j++)
			for(int k=0;k<=a[i];k++)
				for(int l=0;l<=k;l++)
					if(l<=j){
						int nw=j-l+(a[i]-k);
						f[c][nw]=(f[c][nw]+f[c-1][j]*C[a[i]-1][k-1]%mod*C[j][l]%mod*C[sum+1-j][k-l])%mod;
					}
		sum+=a[i];
	}
	cout<<f[c][0]<<endl;
	return 0;
}