近期做的dp題整理

一. 張老師的旅行

鏈接

題目描述

張老師到了一個王國去旅遊,王國有n個景點,張老師到達這個城市所在的車站恰好位於第x個景點,這個王國非常特別,恰好所有著名的景點都在分佈在直線上,每個景點在座標pi上(單位:公里),張老師身體非常好,每走一公里花費一分鐘。每個景點都有一個打卡點,並且必須在不遲於相應的時間(時間從張老師到達王國開始計算)前到達才能打卡成功並且給以一個打卡標記,集齊所這些標記就能獲得一個大禮包。由於張老師非常想要大禮包,並且因爲張老師還着急去下一個王國旅遊,所以張老師希望用的時間儘量少,你能幫幫張老師嗎?

輸入描述

輸入的第一行,包含一個整數n(1≤n≤1000)。
第二行包含n個整數pi(1≤pi≤100000),第i個整數pi爲第i個景點的座標(座標從小到大排列)。
最後一行包含n個整數ti(0≤ti≤10,000,000),ti表示第i個景點最遲到達的時間,時間爲0則表示張老師所在車站的位置且只有一個爲0。

Solution

把所有的點擺在數軸上,整個數軸被起點分成兩部分,我們將左側和右側點分別按照與起點的距離排序,區間被分爲 …3 2 1 0 1 2 3…。我們走過的必須是連續的區間[i , j](左側訪問了i個右側訪問了j個),每次的決策就是終點在左側還是在右側。於是我們考慮dp。

設dp[i][j][0]:終點落在左側的前提下左側訪問完i個右側訪問完j個所用的最少時間。
設dp[i][j][1]:終點落在右側的前提下左側訪問完i個右側訪問完j個所用的最少時間。

dp[i][j][0] = min(dp[i - 1][j][0] + 左側第i - 1個點到第i個點的距離,dp[i - 1][j][1] + 右側第j個點到左側第i個點的距離)
dp[i][j][1] = min(dp[i][j - 1][1] + 右側第j - 1個點到第j個點的距離,dp[i][j - 1][0] + 左側第i個點到右側第j個點的距離)

求出最短時間直接和當前點的截止時間比較判斷是否合法。
兩側都不合法則無法訪問完所有點,直接輸出-1即可。
如果僅有一側不合法,則此種狀態不可用來轉移,把它賦值爲極大值,消除對後面的影響。

代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//__builtin_popcount(n);
#define IOS ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
#define RI register int
const int MOD = 1e9 + 7;
const double PI = acos(-1.0);
const int INF = 0x3f3f3f3f;
const int SZ = 1000 + 10;
int n,k;
int dp[SZ][SZ][2];
struct zt
{
	int dis,ddl;
}a[SZ],l[SZ],r[SZ];
bool cmp(zt x,zt y)
{
	 return x.dis < y.dis;
}
int main()
{
	scanf("%d",&n);
	for(int i = 1;i <= n;i ++) scanf("%d",&a[i].dis);
	for(int i = 1;i <= n;i ++) scanf("%d",&a[i].ddl);
	sort(a + 1,a + n + 1,cmp);
	int lnum = 0,rnum = 0;
	for(int i = 1;i <= n;i ++)
	if(a[i].ddl == 0) 
	{
		k = i;
		break;
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0][0][0] = dp[0][0][1] = 0;
	for(int i = k - 1;i >= 1;i --)
	{
		l[++ lnum].ddl = a[i].ddl; 
		l[lnum].dis = a[k].dis - a[i].dis;
	}
	for(int i = k + 1;i <= n;i ++)
	{
		r[++ rnum].ddl = a[i].ddl;
		r[rnum].dis = a[i].dis - a[k].dis;
	}
	for(int i = 0;i <= lnum;i ++)
		for(int j = 0;j <= rnum;j ++)
		{
			if(i) dp[i][j][0] = min(dp[i - 1][j][0] + l[i].dis - l[i - 1].dis,dp[i - 1][j][1] + l[i].dis + r[j].dis);
			if(j) dp[i][j][1] = min(dp[i][j - 1][1] + r[j].dis - r[j - 1].dis,dp[i][j - 1][0] + l[i].dis + r[j].dis);
			if(dp[i][j][0] > l[i].ddl && dp[i][j][1] > r[j].ddl) 
			{
				printf("-1\n");
				return 0;
			}
			else if(dp[i][j][0] > l[i].ddl) dp[i][j][0] = INF;
			else if(dp[i][j][1] > r[j].ddl) dp[i][j][1] = INF;
		}
	printf("%d\n",min(dp[lnum][rnum][0],dp[lnum][rnum][1]));
	return 0;
} 

二.查查查樂樂

鏈接

題目描述

“查查查樂樂”是一段古老神祕的咒語,只有被選中的魔法師纔有資格使用這一段咒語並享用它所帶來的力量;而如果這段咒語出現在了不具資格的魔法師的口中,這個魔法師將會遭到咒語的反噬並付出可怕的代價。

這個學期,鎂團在一家魔法早教學校做兼職,他的任務是教小學生們魔法並幫助他們準備一年一度的全國魔法奧林匹克競賽 (NOMP)。今天,鎂團在整理圖書的時候,突然發現一本課外教材中包含了 t 段只由查和樂組成的咒語。讓小學生們閱讀這些咒語是非常危險的:他們可能會在無意識中念出“查查查樂樂”。

因此,作爲一名富有責任心的兒童教師,鎂團打算修改這些咒語,從而最大程度地杜絕這方面的隱患。鎂團認爲一段由查和樂組成的咒語是危險的當且僅當在刪去咒語中的若干個字(也可以不刪)後,剩下的咒語可能變成查查查樂樂。舉例來說,“查查查樂樂”,“查查樂查樂樂” 就是危險的,而 “樂樂查查查”,“樂查樂樂查樂查查”就不是危險的。

對於每一段咒語,鎂團都可以選擇若干個位置並對這些位置進行修改:他可以把“查”變成“樂”,也可以把“樂”變成“查”。爲了最大限度地保留教學效果,鎂團希望使用盡可能少的修改來消除所有的危險性:對於每一段咒語,鎂團都希望你幫他計算一下最少的修改次數。

輸入格式

輸入第一行是一個整數 t(1≤t≤1000),表示咒語的數量。

對於每組數據,輸入包含一行一個只包含字符 x 和 l 的字符串 s(1≤|s|≤100),描述了一段咒語。其中 x 表示“查”,l 表示 “樂”。

輸出格式

對於每段咒語,輸出一行一個整數表示最少的修改次數。

Solution

通過修改字符使得咒語中刪除一些字符不能形成"xxxll",求最少修改個數,我們考慮dp。

令dp[i][j] 爲前i個字符中,"xxxll"的最長前綴爲j個字符,需要的最少修改次數.
答案則在dp[i][0 - 4]中

dp[i][j] = min(dp[i - 1][j] + (s[i] == xxxll[j]) , dp[i - 1][j - 1])

代碼

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

char s[105];
int dp[105][6];
int xxxll[5] = {'x','x','x','l','l'};
int main()
{
	int T;
	scanf("%d",&T);
	while(T --)
	{
		memset(dp,0x3f,sizeof(dp));
		scanf("%s",s + 1);
		dp[0][0] = 0;
		int len = strlen(s + 1);
		for(int i = 1;i <= len;i ++)
		{
			for(int j = 0;j < 5;j ++)
			{
				dp[i][j] = dp[i - 1][j] + (s[i] == xxxll[j]);
				if(j) dp[i][j] = min(dp[i][j],dp[i - 1][j - 1]);
			} 
		}
		int ans = 1e9;
		for(int i = 0;i <= 4;i ++)
		ans = min(dp[len][i],ans);
		printf("%d\n",ans);
	}
	return 0;
}

三. 琪露諾

鏈接

題目描述

在幻想鄉,琪露諾是以笨蛋聞名的冰之妖精。

某一天,琪露諾又在玩速凍青蛙,就是用冰把青蛙瞬間凍起來。但是這隻青蛙比以往的要聰明許多,在琪露諾來之前就已經跑到了河的對岸。於是琪露諾決定到河岸去追青蛙。

小河可以看作一列格子依次編號爲0到N,琪露諾只能從編號小的格子移動到編號大的格子。而且琪露諾按照一種特殊的方式進行移動,當她在格子i時,她只移動到區間[i+l,i+r]中的任意一格。你問爲什麼她這麼移動,這還不簡單,因爲她是笨蛋啊。

每一個格子都有一個冰凍指數A[i],編號爲0的格子冰凍指數爲0。當琪露諾停留在那一格時就可以得到那一格的冰凍指數A[i]。琪露諾希望能夠在到達對岸時,獲取最大的冰凍指數,這樣她才能狠狠地教訓那隻青蛙。

但是由於她實在是太笨了,所以她決定拜託你幫它決定怎樣前進。

開始時,琪露諾在編號0的格子上,只要她下一步的位置編號大於N就算到達對岸。

輸入格式

第1行:3個正整數N, L, R

第2行:N+1個整數,第i個數表示編號爲i-1的格子的冰凍指數A[i-1]

輸出格式

一個整數,表示最大冰凍指數。保證不超過2^31-1

Solution

單調隊列優化dp

令dp[i]爲到達第i個格子時得到的最大冰凍指數。

dp[i] = max(dp[k]) + A[i]

O(n^2) 在n爲2e5的情況下會超時。
考慮單調隊列優化dp,對於當前節點i,單調隊列維護的是從i - r 到 i - l 的dp值。
實現細節非常像單調隊列的經典題: 滑動窗口

代碼

#include <bits/stdc++.h>
using namespace std;
const int SZ = 5e6 + 10;
const int INF = 1e9 + 7;
int A[SZ],dp[SZ],q[SZ],id[SZ];
int n,ans,l,r;

inline void solve()
{
	scanf("%d%d%d",&n,&l,&r);
	for(int i = 1;i <= n;i ++) dp[i] = -INF;
	for(int i = 0;i <= n;i ++) scanf("%d",&A[i]); 
	int head = 1,tail = 0;
	int now = 0;
	ans = -INF; 
	for(int i = 1;i <= n;i ++)
	{
		while(now <= i - l) 
		{
			while(head <= tail && dp[now] >= q[tail]) tail --;
			q[++ tail] = dp[now];
			id[tail] = now ++;
		}	
		while(head <= tail &&  id[head] < i - r) head ++;
		if(head <= tail)
			dp[i] = q[head] + A[i]; 
	}
	for(int i = n + 1 - r;i <= n;i ++) ans = max(ans,dp[i]);
	printf("%d\n",ans);
	//printf("dp[1] = %d\n",dp[1]);
}

int main()
{
	solve();
	return 0;
} 

四.任務安排

鏈接

題目描述

n 個任務排成一個序列在一臺機器上等待完成(順序不得改變),這 n 個任務被分成若干批,每批包含相鄰的若干任務。
從零時刻開始,這些任務被分批加工,第 i個任務單獨完成所需的時間爲 ti。在每批任務開始前,機器需要啓動時間 s,而完成這批任務所需的時間是各個任務需要時間的總和(同一批任務將在同一時刻完成)。
每個任務的費用是它的完成時刻乘以一個費用係數 fi。請確定一個分組方案,使得總費用最小。

Solution

斜率優化dp

令dp[i]代表前i個任務分成若干批產生的最小費用
分的批數無法確定,爲了避免算後面的時間要用到前面分了多少批這個狀態,採用費用提前,在處理前面時把S對後面任務產生的費用給計算了。

dp[i] = min( dp[j] + pret[i] * ( pref[i] - pref[j] ) ) + ( pref[n] - pref[j] ) * s

化簡得:
dp[j] = dp[i] + pref[j] * ( s + pret[i] ) - s * pref[n] - pret[i] * pref[i]
把min去掉,把j的取值集合所對應的dp[j]和pref[j]分別作爲函數的f(x)和x
一次函數的斜率k爲(s + pret[i]),截距b爲dp[i]−pret[i] ∗ pref[i] − s ∗ pref[n]
爲了使得dp[i]最小,讓截距b最小即可。

拿一個已知斜率的線上移,第一次碰到的(pref[j],dp[j]),就取到了b的最小值。
用單調隊列維護一個點集,相鄰點的斜率k是遞增的。決策時二分點集,找到最優轉移點即可。
但是本題k是遞增的,所以維護隊首即可,使得每次都是從隊首轉移,當前斜率大於隊首與第二個點之間的斜率時,出隊,之後的隊首元素用來轉移。
統計完答案後再用當前狀態更新隊尾的元素。

學習自:https://www.cnblogs.com/butterflydew/p/9319788.html

代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int SZ = 5e3 + 7;
const int INF = 1e9 + 7;
int pref[SZ],pret[SZ],t[SZ],f[SZ],n,s,q[SZ];
int dp[SZ];

inline void solve()
{
	scanf("%d%d",&n,&s);
	for(int i = 1;i <= n;i ++)	
	{
		scanf("%d%d",&t[i],&f[i]);
		pret[i] = pret[i - 1] + t[i];
		pref[i] = pref[i - 1] + f[i];
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0] = 0;
	int l = 1,r = 1;
	for(int i = 1;i <= n;i ++)
	{
		while(l < r && dp[q[l + 1]] - dp[q[l]] <= (s + pret[i]) * (pref[q[l + 1]] - pref[q[l]])) l ++;
			dp[i] = dp[q[l]] + pret[i] * pref[i] + s * pref[n] - pref[q[l]] * (s + pret[i]);
		while(l < r && (dp[i] - dp[q[r]]) * (pref[q[r]] - pref[q[r - 1]]) <= (dp[q[r]] - dp[q[r - 1]])*(pref[i] - pref[q[r]])) 
			r --;	
		q[++ r] = i;
	}
	printf("%d\n",dp[n]);
}

int main()
{
	solve();
	return 0;	
}
 

五.Emiya 家今天的飯

鏈接

題目描述

Emiya 是個擅長做菜的高中生,他共掌握 n 種烹飪方法,且會使用 m 種主要食材做菜。爲了方便敘述,我們對烹飪方法從1∼n 編號,對主要食材從1∼m 編號。
Emiya 做的每道菜都將使用恰好一種烹飪方法與恰好一種主要食材。更具體地,Emiya 會做 ai,j道不同的使用烹飪方法 i 和主要食材 j 的菜(1≤i≤n,1≤j≤m),這也意味着 Emiya 總共會做 ∑∑ai,j道不同的菜。
Emiya 今天要準備一桌飯招待 Yazid 和 Rin 這對好朋友,然而三個人對菜的搭配有不同的要求,更具體地,對於一種包含 k道菜的搭配方案而言:
1.Emiya 不會讓大家餓肚子,所以將做至少一道菜,即k≥1
2.Rin 希望品嚐不同烹飪方法做出的菜,因此她要求每道菜的烹飪方法互不相同
3.Yazid 不希望品嚐太多同一食材做出的菜,因此他要求每種主要食材至多在一半的菜(即 k/2(下取整) 道菜)中被使用
這些要求難不倒 Emiya,但他想知道共有多少種不同的符合要求的搭配方案。兩種方案不同,當且僅當存在至少一道菜在一種方案中出現,而不在另一種方案中出現。
Emiya 找到了你,請你幫他計算,你只需要告訴他符合所有要求的搭配方案數對質數 998,244,353 取模的結果。

題目大意

給出一個矩陣,要求每行只能選一個節點,每列選的節點不能超過所有選的節點的一半,不能不選,給出每個節點的選擇方案數,求總方案數

Solution

dp + 容斥 + 減去無用狀態

不合法列最多隻有1列,那很容易可以計算列不合法時的方案數:每行選不超過一個的方案數 - 每行選不超過一個,且某一列選了超過一半的方案數。

總方案:
令dp[i][j]爲前i行選了j個數的方案數
dp[i][j] = dp[i - 1][j] + sum[i] * dp[i - 1][j - 1]

列非法方案:
令f[i][j]爲表示前i行,當前列(now)的數比其他列的數多了j個的方案數
f[i][j] = f[i - 1][j] + f[i - 1][j - 1] * a[i][now] + f[i - 1][j + 1] * (sum[i] - a[i][now])

狀態數n^2,枚舉當前行m.
總時間複雜度爲O(m * n^2)

代碼

#include <bits/stdc++.h>
using namespace std;
const int SZ = 2000 + 20;
const int INF = 0x3f3f3f3f;
const int MOD = 998244353;
typedef long long ll;
ll n,m;  
ll a[105][SZ],f[105][105 * 2],sum[105][SZ]; 
ll dp[105][105];
inline void solve()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
		{
			scanf("%lld",&a[i][j]);
			sum[i][0] = (sum[i][0] +  a[i][j]) % MOD;
		}
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
			sum[i][j] = (sum[i][0] - a[i][j] + MOD) % MOD;
	ll ans = 0; 
	dp[0][0] = 1;
	for(int i = 1;i <= n;i ++)
		for(int j = 0;j <= n;j ++)
		{
			if(j > 0) dp[i][j] = (dp[i - 1][j] + (dp[i - 1][j - 1] * sum[i][0]) % MOD ) % MOD;
			else dp[i][j] = dp[i - 1][j];
		}
	for(int i = 1;i <= n;i ++)
		ans = (ans + dp[n][i]) % MOD; 
	for(int col = 1;col <= m;col ++)
	{
		memset(f,0,sizeof(f));
		f[0][n] = 1; // n == 0 
		for(int i = 1;i <= n;i ++)
			for(int j = n - i;j <= n + i;j ++)
				f[i][j] = (f[i - 1][j] + (f[i - 1][j - 1] * a[i][col]) % MOD + (f[i - 1][j + 1] * sum[i][col]) % MOD) % MOD;		
		for(int i = 1;i <= n;i ++)
			ans = (ans - f[n][n + i] + MOD) % MOD;
	}
	printf("%lld\n",ans % MOD);
} 

int main()
{
	solve();
	return 0;	
}

六.K-periodic Garland

鏈接

題目描述

給定長度爲n的01串,每次操作可以改變一個字符的狀態,問使字符串中相鄰1的距離爲k的最小操作次數

Solution

令dp[i][0]爲使得前i位都合法,第i位爲0的最小操作數

令dp[i][1]爲使得前i爲都合法,第i位爲1的最小操作數

dp[i][0] = min(dp[i - 1][0],dp[i - 1][1]) + (s[i] == ‘1’)

pre[i]爲前i個字符中1的個數

dp[i][1] = min(dp[p][1] + pre[i - 1] - pre[p],pre[i - 1]) + (s[i] == ‘0’)

代碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//__builtin_popcount(n);
#define IOS ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
#define RI register int
const int MOD = 1e9 + 7;
const double PI = acos(-1.0);
const int INF = 0x3f3f3f3f;
const int SZ = 2e6 + 10;
int n,k;
int dp[SZ][2],pre[SZ];//前i項合法且第i項爲j 
char s[SZ]; 
inline void solve()
{
	for(int i = 1;i <= n;i ++)	pre[i] = pre[i - 1] + (s[i] == '1');
	for(int i = 1;i <= n;i ++)
	{
		int p = max(0,i - k);
		dp[i][0] = min(dp[i - 1][0],dp[i - 1][1]) + (s[i] == '1');
		dp[i][1] = min(dp[p][1] + pre[i - 1] - pre[p],pre[i - 1]) + (s[i] == '0'); 
	}
	printf("%d\n",min(dp[n][0],dp[n][1])); 
	for(int i = 1;i <= n;i ++)
	dp[i][0] = dp[i][1] = pre[i] = 0;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T --)
	{
		scanf("%d%d",&n,&k);
		scanf("%s",s + 1);
		solve();	
	}
	return 0;
} 

2020.6.3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章