近期做的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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值