DP练习

题目来源:https://www.cnblogs.com/henry-1202/p/9211398.html#_label0
蒟蒻照搬的题目orz 各位要学习的话建议click ↑↑

文章目录

  • 1.合唱队形
  • 2.导弹拦截
  • 3.尼克的任务
  • 4.丝绸之路
  • 5.分队问题
  • 6.低价购买
  • 7.回文子串
  • 8.最长公共子序列
  • 9.魔族秘密
  • 10.创意吃鱼法
  • 11.王子和公主
  • 12.木棍加工
  • 13.跨河
  • 14.照明系统设计
  • 15. 出租车拼车
  • 16.最佳课题选择
  • 17.奶牛零食
  • 18.能量项链
  • 19.跳房子
  • 20.摆花
  • 21.摆渡车
  • 22.POGO的牛
  • 23.乌龟棋
  • 24.跑步
  • 25.过河
  • 26.牛的词汇

1.合唱队形

传送门
队形长度即为:
最长上升子序列+最长下降子序列 -1

#include
using namespace std;
int n,up[110],down[110];
int h[110]; 
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		cin>>h[i];
	for(int i=1;i<=n;++i)
		up[i]=1,down[i]=1;
	for(int i=n-1;i>=1;--i)
		for(int j=i+1;j<=n;++j)
			if(h[i]>h[j]) down[i]=max(down[i],down[j]+1);
	for(int i=2;i<=n;++i)
		for(int j=i-1;j>=1;--j)
			if(h[j]<h[i]) up[i]=max(up[i],up[j]+1);
	int ans=0;
	for(int i=1;i<=n;++i)
		ans=max(ans,down[i]+up[i]-1);
	cout<<n-ans;
}

2.导弹拦截

传送门
系统最多拦截弹导数:最长不下降子序列
最少拦截所有导弹系统数:最长上升子序列

n^2做法(传统做法)

#include
using namespace std;
int h[110000],down[110000],up[110000];
int main()
{
	int n=0;
	for(int i=1;scanf("%d",&h[i])!=EOF;++i)
		down[i]=1,up[i]=1,++n;
	for(int i=2;i<=n;++i)
		for(int j=i-1;j>=1;--j)
		{
			if(h[j]>=h[i]) down[i]=max(down[i],down[j]+1);
			if(h[j]<h[i]) up[i]=max(up[i],up[j]+1);
		}
	int num=0,cnt=0;
	for(int i=1;i<=n;++i)
		num=max(num,down[i]),   //这套系统最多拦截导弹数 
		cnt=max(cnt,up[i]);     //最少拦截导弹系统数 
	cout<<num<<endl<<cnt;   
}

n l o g n nlog_n nlogn做法(STL函数 二分更快 )这种算法只能求长度,无法输出子序列
后面写最长公共子序列有写 二分模板

#include 
#include 
using namespace std;
const int N = 100010;
int a[N], d1[N], d2[N], n;
int main()
{
	while (cin>>a[++n]);
	n--;		//输入
	int len1=1, len2= 1;		//初始长度为1
	d1[1]=a[1];		//用于求不上升序列长度
	d2[1]=a[1];		//用于求上升序列长度
	for(int i=2; i<=n; i++) 
	{		//从a[2]开始枚举每个数(a[1]已经加进去了)
		if(d1[len1]>=a[i]) d1[++len1]=a[i];		//如果满足要求(不上升)就加入d1
		else
		{		//否则用a[i]替换d1中的一个数
			int p1=upper_bound(d1+1, d1+1+len1,a[i],greater<int>())-d1;
			d1[p1]=a[i]; 
		}
		if (d2[len2]<a[i]) d2[++len2]=a[i];		//同上
		else 
		{
			int p2=lower_bound(d2+1, d2+1+len2,a[i])-d2;
			d2[p2]=a[i];
		}
	}
	cout<<len1<<endl<<len2;		//输出
	return 0;		//结束
}

3.尼克的任务

传送门
显然动态转移方程:

( 本 时 刻 无 任 务 ) f [ i ] = f [ i + 1 ] + 1 ( 本 时 刻 有 任 务 ) f [ i ] = m a x ( f [ i ] , f [ i + a [ s u m ] ) (本时刻无任务)f[i]=f[i+1]+1\\ (本时刻有任务)f[i]=max(f[i],f[i+a[sum]) f[i]=f[i+1]+1f[i]=max(f[i],f[i+a[sum])
可见状态f[i]与后面的时刻有关 ⇒ \Rightarrow 倒着递推
*在处理方面:先将任务排序,用从大到小用任务个数枚举

#include
using namespace std;
const int N=11000;
int n,k,p[N],t[N],cnt[N],f[N];
struct task
{
	int p,t;
} z[N];
bool cmp(task x,task y)
{
	return x.p<y.p;	
}
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=k;++i)
	{
		scanf("%d%d",&z[i].p,&z[i].t);
		++cnt[z[i].p];
	}
		 
	sort(z+1,z+1+k,cmp);
	int num=k;
	for(int i=n;i>=1;--i)
	{
		if(!cnt[i]) f[i]=f[i+1]+1;
		else 
			for(int j=1;j<=cnt[i];++j)
			{
				f[i]=max(f[i],f[i+z[num].t]);
				num--;
			}
	}
	cout<<f[1];
}

4.丝绸之路

传送门
f[i][j] 表示到第i个城市时为第j天
由题意可知 有两种状态:
移 动 : f [ i − 1 ] [ j − 1 ] + d [ i ] ∗ c [ j ] ( 昨 天 在 i − 1 城 市 , 今 天 走 到 i 城 市 ) 休 息 : f [ i ] [ j − 1 ] f [ i − 1 ] [ j − 1 ] + d [ i ] ∗ c [ j ] ( 昨 天 就 到 了 i 城 市 , 在 i 城 市 休 息 了 一 天 ) 转 移 方 程 : f [ i ] [ j ] = m i n ( 移 动 , 休 息 ) 移动:f[i-1][j-1]+d[i]*c[j](昨天在i-1城市,今天走到i城市)\\ 休息:f[i][j-1]f[i-1][j-1]+d[i]*c[j](昨天就到了i城市,在i城市休息了一天)\\ 转移方程:f[i][j]=min(移动,休息) f[i1][j1]+d[i]c[j]i1,i)f[i][j1]f[i1][j1]+d[i]c[j]i,if[i][j]=min()

#include
using namespace std;
const int N=1100,M=1100;
int n,m,f[N][M],d[N],c[M];
void dp()
{
	memset(f,0x7f,sizeof(f));         //初始化最大值 
	for(int i=0;i<=m;i++) f[0][i]=0;  //初始化边界值 
	
	for(int i=1;i<=n;++i)
		for(int j=i;j<=m-n+i;++j)
		{
			int rest=f[i][j-1],       //昨天就到了i城市,在i城市休息了一天 
				move=f[i-1][j-1]+d[i]*c[j]; //昨天在i-1城市,今天走到i城市 
			f[i][j]=min(rest,move);
		}
} 
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",&d[i]);
	for(int i=1;i<=m;++i)
		scanf("%d",&c[i]);
	dp();
	cout<<f[n][m];
}

5.分队问题

传送门
u1s1第一眼确实没想到dp
f [ i ] f[i] f[i]表示有i个人时可满足的最多队伍数
(在dp循环中 i也表示此时在给第n个人编队)
要把容易满足的放在最前面编队所以要先排序(不会影响结果)
那么对于i有两种状态:
i < a [ i ] 时     f [ i ] = f [ i ] ii<a[i]   f[i]=f[i]第i个人没有选择只能直接加入之前的队列
i ≥ a [ i ] 时     f [ i ] = m a x ( f [ i − 1 ] , f [ i − a [ i ] ) i\ge a[i]时\ \ \ f[i]=max(f[i-1],f[i-a[i]) ia[i]   f[i]=max(f[i1],f[ia[i])第i个人的要求已经可以满足了,他可以拉a[i]个人出去单独编一队,也可以继续加入原队

#include
using namespace std;
const int N=1e6+10;
int n,a[N],f[N];
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	sort(a+1,a+1+n);  //从小到大排序 要求数目没变不影响结果 
	for(int i=1;i<=n;++i)
	{
		if(a[i]>i) 
			f[i]=f[i-1];
		else //a[i]<=i此时满足i最少人数要求了
			f[i]=max(f[i-a[i]]+1,f[i-1]); 
			//i拉a[i]个人单独组队 或 直接加入原来的队 
	}
	cout<<f[n]; 
}

6.低价购买

传送门
题目有两个问题:最大购买次数 和 最大购买次数的方案数
第一个问题 最长下降子序列 上面全是模板(用 d o w n [ i ] down[i] down[i]表示)
第二个问题 实际上也是一种dp
sum[i]表示以i节点为头,长度为 d o w n [ i ] down[i] down[i]的子序列的个数(最终答案就是累加 d o w n [ i ] down[i] down[i]为最大值时的 s u m [ i ] sum[i] sum[i]
s u m [ i ] sum[i] sum[i]为所有 接在i后面的序列的$sum[] $的总值
a [ i ] > a [ j ] 且 d o w n [ i ] = d o w n [ i ] + 1      s u m [ i ] + = s u m [ j ] a[i]>a[j]且 down[i]=down[i]+1 \ \ \ \ sum[i]+=sum[j] a[i]>a[j]down[i]=down[i]+1    sum[i]+=sum[j]
题意还要求去除重复的序列,由于我们在求LIS时只保存了序列的长度和开头,则考虑开头相同且长度也相同的序列去重
a [ i ] = a [ j ] 且 d o w n [ i ] = d o w n [ i ]      s u m [ i ] = 0 a[i]=a[j]且 down[i]=down[i]\ \ \ \ sum[i]=0 a[i]=a[j]down[i]=down[i]    sum[i]=0

不知道为什么把 求sum 和 去重 分开写是错的只好合在一起

#include
using namespace std;
const int N=5e3+10;
long long n,a[N],down[N],sum[N];
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)
		down[i]=1;  //初始化down[]边界值 
	for(int i=n;i>=1;--i)
		for(int j=i+1;j<=n;++j)
			if(a[i]>a[j]) 
				down[i]=max(down[i],down[j]+1);
		//求最长下降子序列	
		for(int i=1;i<=n;++i)
			if(down[i]==1) sum[i]=1; //初始化sum[]边界值 
	for(int i=n;i>=1;--i) 
		for(int j=i+1;j<=n;++j)
			if(a[i]>a[j]&&down[i]==down[j]+1) sum[i]+=sum[j];
			else if(a[i]==a[j]&&down[i]==down[j]) sum[j]=0;
		//求以i节点为头,长度为down[i]的子序列 的个数 
		//除去重复的序列 
	long long m=0,ans=0;
	for(int i=1;i<=n;++i)
		m=max(m,down[i]);
	for(int i=1;i<=n;++i)
		if(down[i]==m) ans+=sum[i];
	cout<<m<<" "<<ans;		
}

7.回文子串

传送门
因为回文的性质,我们想到把原串先倒序复制一份,那么两个字符串的最长公共子序列就是原串已经满足回文的部分
需要添加的长度即 原串长度-最长公共子序列的长度
问题转化为求最长公共子序列:
i f ( c h [ i ] = c h [ j ] )     f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 e l s e     f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) if(ch[i]=ch[j])\ \ \ f[i][j]=f[i-1][j-1]+1\\ else\ \ \ f[i][j]=max(f[i-1][j],f[i][j-1]) if(ch[i]=ch[j])   f[i][j]=f[i1][j1]+1else   f[i][j]=max(f[i1][j],f[i][j1])

#include
using namespace std;
const int N=1e3+10;
int f[N][N];
char s1[N],s2[N];
int main()
{
	gets(s1+1); int n=strlen(s1+1);
	for(int i=1;i<=n;++i)
		s2[i]=s1[n-i+1];  //倒序复制s1到s2 
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			if(s1[i]==s2[j]) f[i][j]=f[i-1][j-1]+1;
			else f[i][j]=max(f[i-1][j],f[i][j-1]);
	cout<<n-f[n][n]; 
}

但是对于二维数组空间复杂度着实吓人
我们观察到 f [ i ] [ j ] f[i][j] f[i][j] 数组的第一维只在 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] f [ i − 1 ] [ j − 1 ] f[i-1][j-1] f[i1][j1] 中用到了
所以第一维可以用一个滚动数组 f 2 f_2 f2代替 其实我也不知道这是啥
f 2 [ ] = f [ i − 1 ] [ ] f_2[]=f[i-1][] f2[]=f[i1][]

#include
using namespace std;
const int N=1e3+10;
int f1[N],f2[N];  //f2[]就相当于f[i-1][] 
char s1[N],s2[N];
int main()
{
	gets(s1+1); int n=strlen(s1+1);
	for(int i=1;i<=n;++i)
		s2[i]=s1[n-i+1];  //倒序复制s1到s2 
	for(int i=1;i<=n;++i)
	{ 
		for(int j=1;j<=n;++j)
			if(s1[i]==s2[j]) f1[j]=f2[j-1]+1; //f[i-1][j-1]变为f2[j-1] 
			else f1[j]=max(f2[j],f1[j-1]); //f[i-1][j]变为f2[j] 
		memcpy(f2,f1,sizeof(f1));
	}
	cout<<n-f1[n];
}

然后下一道题就是最长公共子序列的 n l o g n nlog_n nlogn

8.最长公共子序列

传送门
最长公共子序列的 n l o g n nlog_n nlogn版实际上是从最长上升子序列转换过来的
我们把数字的大小按照第一个序列从左到右 是 从小到大 的规则
求出第二个序列在此规则下的上升子序列就是公共子序列了
(第二个序列中若有元素不属于第一个序列直接不管就是,反正肯定不是公共子序列)
上升子序列 我写过STL函数的写法 这里就手写二分

#include
using namespace std;
const int N=1e5+10;
int n,a[N],b[N],m[N];
int sstack[N],top;  //手写栈 
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&a[i]),m[a[i]]=i;  //m数组用以对应a[i]的序号 
	for(int i=1;i<=n;++i)
		scanf("%d",&b[i]);
	for(int i=1;i<=n;++i)
		b[i]=m[b[i]];   //用m[]对应转化第二个序列 
	for(int i=1;i<=n;++i)
	{
		if(b[i]>sstack[top]) sstack[++top]=b[i];
		else
		{
			int l=1,r=top;
			while(l<r)
			{
				int mid=(l+r)>>1;
				if(b[i]>sstack[mid]) l=mid+1;
				else r=mid;
			}
			sstack[l]=b[i];
		}
	}
	cout<<top;
}

9.魔族秘密

传送门
如果把a是b的子串转化为a 转化方法:hash,string(函数),char(strstr函数)…
我这里写的是strstr函数
这不应该是一道字典树题嘛…

#include
using namespace std;
const int N=2e3+10,M=80;
int n,f[N];
char s[N][M];
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		scanf("%s",s[i]);
	for(int i=1;i<=n;++i)
		f[i]=1;      //差点又忘了初始化 
	for(int i=n;i>=1;--i)
		for(int j=i+1;j<=n;++j)
			if(strstr(s[j],s[i])==s[j]) f[i]=max(f[i],f[j]+1);
		     //即表示i是j的子串
	int ans=0;
	for(int i=1;i<=n;++i)
		ans=max(ans,f[i]);
	cout<<ans;
}

10.创意吃鱼法

传送门
有点毒瘤
f 1   f 2 f_1\ f_2 f1 f2分别维护向右斜和向左斜,以 ( i , j ) (i,j) (i,j)为终止的连续的1的个数
则可以两层循环dp,对每一个满足 a [ i ] [ j ] = 1 a[i][j]=1 a[i][j]=1的点,有:
f 1 [ i ] [ j ] = m i n ( f 1 [ i − 1 ] [ j − 1 ] , m i n ( c n t 1 [ i ] [ j − 1 ] , c n t 2 [ i − 1 ] [ j ] ) ) + 1 f 2 [ i ] [ j ] = m i n ( f 2 [ i − 1 ] [ j + 1 ] , m i n ( c n t 2 [ i − 1 ] [ j ] , c n t 3 [ i ] [ j + 1 ] ) ) + 1 , f1[i][j]=min(f1[i-1][j-1],min(cnt1[i][j-1],cnt2[i-1][j]))+1\\ f2[i][j]=min(f2[i-1][j+1],min(cnt2[i-1] [j],cnt3[i][j+1]))+1, f1[i][j]=min(f1[i1][j1],min(cnt1[i][j1],cnt2[i1][j]))+1f2[i][j]=min(f2[i1][j+1],min(cnt2[i1][j],cnt3[i][j+1]))+1,
那么很关键的 c n t 1   2   3 [ i ] [ j ] cnt1\ 2\ 3[i][j] cnt1 2 3[i][j]则是需要预处理的数组用以判断在递推时最多能够连上几个1
(题上说正方形对角线之外的数字必须是0 不会只有我没看到吧
c n t 1   2   3 cnt1\ 2\ 3 cnt1 2 3分别表示从 ( i , j ) (i,j) (i,j)向左 上 右 连续0的个数
还有一根问题这么多N*M的数组空间复杂度很危险 于是在这里引入 s h o r t short short 前提是数字都不超过32767变量

#include
using namespace std;
const int N=2600,M=2600;
short n,m,a[N][M],ans;
short f1[N][M],f2[N][M];
//f1表示(i,j)为末,向右向下连续1个数 
//f2表示(i,j)为末,向左向下连续1个数 
short cnt1[N][M],cnt2[N][M],cnt3[N][M];
//cnt1表示向左数 连续0的个数
//cnt2表示向上数 连续0的个数
//cnt3表示向右数 连续0的个数
short read()  //读入优化
{
	short x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;  //我每次写读入优化都要忘这个 
} 
int main()
{
	n=read();m=read();
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
		{
			a[i][j]=read();
			if(!a[i][j])
				cnt1[i][j]=cnt1[i][j-1]+1,
				cnt2[i][j]=cnt2[i-1][j]+1;  //预处理左 上方向0的个数 
		}
	for(int i=1;i<=n;++i)
		for(int j=m;j>=1;--j)
			if(!a[i][j])
				cnt3[i][j]=cnt3[i][j+1]+1; //预处理右方向0的个数
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			if(a[i][j])
			{
				f1[i][j]=min(f1[i-1][j-1],min(cnt1[i][j-1],cnt2[i-1][j]))+1,
				f2[i][j]=min(f2[i-1][j+1],min(cnt2[i-1][j],cnt3[i][j+1]))+1,
				//同时满足三者条件所以用min 
				ans=max(max(f1[i][j],f2[i][j]),ans);	
			}
		cout<<ans;
}

11.王子和公主

(Prince and Princess)
传送门(luogu)
传送门(VJ)
这题就是求最长公共子序列,详情请见8题模板
(交到VJ上 不明原因T了望大佬康康)

#include
using namespace std;
const int N=1e5+10;
int nn,n,m,a[N],b[N],M[N];
int sstack[N],top;  //手写栈 
int main()
{
	int t;
	scanf("%d",&t);
	for(int k=1;k<=t;++k)
	{
		memset(M,0,sizeof(M));
		top=0;
		scanf("%d%d%d",&nn,&n,&m);
		for(int i=1;i<=n+1;++i)
			scanf("%d",&a[i]),M[a[i]]=i;  //M数组用以对应a[i]的序号 
		for(int i=1;i<=m+1;++i)
			scanf("%d",&b[i]);
		for(int i=1;i<=m+1;++i)
			b[i]=M[b[i]];   //用M[]对应转化第二个序列 
		for(int i=1;i<=m+1;++i)
		{
			if(!b[i]) continue;
			if(b[i]>sstack[top]) sstack[++top]=b[i];
			else
			{
				int l=1,r=top;
				while(l<r)
				{
					int mid=(l+r)>>1;
					if(b[i]>sstack[mid]) l=mid+1;
					else r=mid;
				}
				sstack[l]=b[i];
			}
		}
		printf("Case %d:%d",k,top);
	}
}

12.木棍加工

传送门
求 l和w同时单调下降的序列 的最小个数
显然先按照一个关键字排序,此时另一个关键字下降子序列的最小划分即为答案
下降子序列的最小划分 = = =最长上升子序列 (反之亦然)拦截导弹还记得吧
懒得打 n l o g n nlog_n nlogn就直接 n 2 n^2 n2

#include
using namespace std;
const int N=5e3+10;
int n,up[N];
struct stick
{
	int l,w;
} a[N];
bool cmp(stick x,stick y)
{ return x.l>y.l; }
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		cin>>a[i].l>>a[i].w;
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;++i)
		up[i]=1;   //初始化边界值差点又忘了 
	for(int i=n;i>=1;--i)
		for(int j=i+1;j<=n;++j)
			if(a[i].w<a[j].w) 
				up[i]=max(up[i],up[j]+1); 
	//求最长上升子序列 即 下降子序列的最小划分
	int ans=0;
	for(int i=1;i<=n;++i)
		ans=max(ans,up[i]);
	cout<<ans;
}

13.跨河

(River Crossing S)
传送门
f [ i ] f[i] f[i]表示 i i i头奶牛的最小花费
从把 j j j i i i枚举到 n n n,
f [ i ] = m i n ( f [ i ] , f [ i − j ] + c o s t [ j ] ) f[i]=min(f[i],f[i-j]+cost[j]) f[i]=min(f[i],f[ij]+cost[j])
注意cost要算上划过去和划回来
但是最后不用划回来,所以答案要减去m

#include
using namespace std;
const int N=2e3+5e2+10,M=1e3+10;
int n,m[N],f[N];
int main()
{
    cin>>n>>m[0];
    for(int i=1;i<=n;++i)
    {
        int x;
        cin>>x;
        m[i]=m[i-1]+x;
    }
    memset(f,0x7f,sizeof(f));
    f[0]=0;  //差点又tm忘了边界值 

    for(int i=1;i<=n;++i)
        for(int j=1;j<=i;++j)
            f[i]=min(f[i],f[i-j]+m[j]+m[0]);
    cout<<f[n]-m[0];
}

另外此题可当做背包来做
运送不同数量的奶牛 = = =不同的物品
奶牛的数量 = = =背包的容量
具体看代码

#include
using namespace std;
const int N=2e3+5e2+10,M=1e3+10;
int n,m[N],f[N];
int main()
{
	cin>>n>>m[0];
	for(int i=1;i<=n;++i)
	{
		int x;
		cin>>x;
		m[i]=m[i-1]+x;
	}
	memset(f,0x7f,sizeof(f));
	f[0]=0;   //差点又tm忘了边界值 
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			f[j]=min(f[j],f[j-i]+m[i]+m[0]);
	cout<<f[n]-m[0];
}

14.照明系统设计

(Lighting System Design)
传送门(luogu)
传送门(VJ)
电压大的灯泡替换电压小的灯泡 显然要先以灯泡的电压为关键字排序
对于某一段灯泡 k k k~~ j j j( j ≤ i j\le i ji) ,全部由i灯泡替换 一定优于 部分由i灯泡替换,部分由其他替换
所以用 f [ i ] f[i] f[i]表示前 i i i个灯泡的最优解,则转移方程: f [ i ] = m i n ( f [ i ] , f [ j ] + c [ i ] ∗ ( j 到 i 的 灯 泡 总 个 数 ) + k [ i ] )      ( j < i ) f[i]=min(f[i],f[j]+c[i]*(j到i的灯泡总个数)+k[i])\ \ \ \ (jf[i]=min(f[i],f[j]+c[i](ji)+k[i])    (j<i)

#include
using namespace std;
const int N=1e3+10;
int n,f[N],sum[N];
struct light
{
	int v,k,c,l;
} a[N];   //用结构体记录四个关键字 
bool cmp(light x,light y)
{	return x.v<y.v; }
int main()
{
	while(cin>>n,n)
	{
		for(int i=1;i<=n;++i)
			scanf("%d%d%d%d",&a[i].v,&a[i].k,&a[i].c,&a[i].l);
		sort(a+1,a+1+n,cmp);        //以灯泡的电压为关键字排序 
		for(int i=1;i<=n;++i) 
			sum[i]=sum[i-1]+a[i].l;	//求灯泡总个数的前缀和 
		memset(f,0x7f,sizeof(f));
		f[0]=0;                     //我说我又把边界值搞忘了你信吗 
		for(int i=1;i<=n;++i)
			for(int j=0;j<i;++j)
				f[i]=min(f[i],f[j]+a[i].k+a[i].c*(sum[i]-sum[j]));
		cout<<f[n]<<endl;
	}
}

15. 出租车拼车

传送门
这题依旧是一个背包 出租车是物品 人数是背包容量 但是还需要比普通的背包加一维枚举第 i i i辆车坐几个人(注意取min,防止下标为负)
则转移方程: f [ j ] = m i n ( f [ j ] , f [ j − k ] + t [ i ] ∗ k + D ) f[j]=min(f[j],f[j-k]+t[i]*k+D) f[j]=min(f[j],f[jk]+t[i]k+D)

#include
using namespace std;
const int N=1e2+10;
int n,m,d,s;
int f[N];
struct car
{
	int t,z;
} a[N];
int main()
{
	cin>>n>>m>>d>>s;
	for(int i=1;i<=m;++i)
		cin>>a[i].t>>a[i].z; 
		
	memset(f,0x7f,sizeof(f));
	f[0]=0;      //dp边界值 这回我记住了 
	
	for(int i=1;i<=m;++i)
		for(int j=n;j>=1;--j) //相当于01背包 一维滚动数组必须倒序循环 
		{
			int num=min(a[i].z,j);  
			//num表示乘i车最多能走多少人 (避免下标为负) 
			for(int k=1;k<=num;++k)
				f[j]=min(f[j],f[j-k]+d+k*a[i].t);
		}
	if(f[n]>30000) cout<<"impossible";  
	else cout<<f[n];
}

16.最佳课题选择

传送门
背包问题,论文 = = =背包容量,课题 = = =物品
由于一个课题可以占用多个论文,所以需要加上一层循环枚举此课题占用多少论文
所以这不就熟多重背包
但是注意,此题因为权值的计算方式 占用不同数量论文的同一个课题不能简单的相加(占用一个论文 和 占用两个论文 相加不能等同于 占用三个论文)
所以 k k k循环必须放在第三层,二进制拆分这题是不能用的
我就挂在这个神奇的地方

#include
using namespace std;
const int N=210,M=30;
int n,m,a[M],b[M];
long long f[N]; 
long long B(int x,int y)
{
	long long ans=1;
	for(int i=1;i<=y;++i)
		ans*=x;
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
		scanf("%d%d",&a[i],&b[i]);
	memset(f,0x7f,sizeof(f));
	f[0]=0;     //边界值!!! 
	for(int i=1;i<=m;++i)
		for(int j=n;j>=k;--j)
			for(int k=1;k<=j;++k)  //枚举放入几个i课题 
				f[j]=min(f[j],f[j-k]+(long long)a[i]*B(k,b[i]));
	printf("%lld",f[n]);
}

17.奶牛零食

(Treats for the Cows G/S)
传送门
每次卖出数列第一个或者最后一个,那么转移方程就很显然了
f [ i ] [ j ] f[i][j] f[i][j]表示区间 [ i , j ] [i,j] [i,j]最大的收益,则: f [ i ] [ j ] = m a x ( f [ i + 1 ] [ j ] + v a l [ i ] , f [ i ] [ j − 1 ] + v a l [ j ] ) f[i][j]=max(f[i+1][j]+val[i],f[i][j-1]+val[j]) f[i][j]=max(f[i+1][j]+val[i],f[i][j1]+val[j])
v a l [ i ] val[i] val[i] i i i卖出的时间有关的,时间是可以计算得到的: d a y = n − ( j − i + 1 ) + 1 day=n-(j-i+1)+1 day=n(ji+1)+1

于是就变成了区间DP下面是我自己对区间dp的理解
在枚举所有区间时,为了保证小区间比大区间更新(因为需要通过小区间更新大区间)
先从小到大枚举区间的长度,再枚举区间起点(长度为1的区间即是边界值)
注:与一般的 两层 1 1 1 n n n循环相比,复杂度相同,但枚举顺序不同

#include
using namespace std;
const int N=2e3+10;
int n,val[N],f[N][N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&val[i]);
	memset(f,0x7f,sizeof(f));
	for(int i=1;i<=n;++i)
		f[i][i]=val[i]*n;  //边界值即为最后只剩一个的区间 
		
	for(int len=2;len<=n;++len)            
		for(int i=1,j=i+len-1;j<=n;++i,++j)//这两层循环就是区间dp吧
		
		{  //便于理解,这里仍然用i,j来表示区间(当然也可以用 i,len组成的的式子 做下标)
			int day=n-len+1;   //day表示是第几天卖出的食物 
			f[i][j]=max(f[i+1][j]+val[i]*day,f[i][j-1]+val[j]*day);
		}
	printf("%d",f[1][n]);
}

18.能量项链

传送门
这题跟石子合并似乎毫无区别…
首先显然是区间dp,然后对于区间 [ i , j ] [i,j] [i,j],枚举一个 k k k 区 间 [ i , j ] 区间[i,j] [i,j]分为 [ i , k ] [i,k] [i,k] [ k + 1 , j ] [k+1,j] [k+1,j]
则转移方程: f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + h e a d [ i ] ∗ t a i l [ k ] ∗ t a i l [ j ] ) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]tail[k]tail[j])

#include
using namespace std;
const int N=110;
int n,head[2*N],tail[2*N];
int f[2*N][2*N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&head[i]),head[i+n]=head[i]; //通过把序列增致2*n来拆环成链 
	for(int i=1;i<=2*n;++i)
		tail[i]=head[i+1];//第i颗珠子的尾标记应该等于第i+1颗珠子的头标记
	tail[2*n]=head[1];	  //第n颗珠子的尾标记应该等于第1颗珠子的头标记
	for(int len=2;len<=n;++len)
		for(int i=1,j=i+len-1;j<=2*n;++i,++j)
			for(int k=i;k<j;++k)
				f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]); 
	int ans=0;
	for(int i=1;i<=n;++i)
		ans=max(ans,f[i][i+n-1]);
	//环拆成双链后,则从1-n任意一个作起点 长度为n的序列,都可能成为答案 
	cout<<ans;
} 

19.跳房子

传送门
这毒瘤题我调了一晚上
看到题面,显然是要用二分答案:二分一个 m i d mid mid为金币数    c h e c k ( m i d ) \ \ check(mid)   check(mid)是否能得到大于k分
然后check()函数应该很容易就想到递推公式了: f [ i ] = m a x   f [ j ≤ i 且 j 能 走 到 i ] + v a l [ i ] f[i]=max\ f[j\le i且j能走到i]+val[i] f[i]=max f[jiji]+val[i]
于是提交,* 50分 *警告, O ( n 2 l o g n ) O(n^2log_n) O(n2logn)显然太大了
由于我们每次的选择是一定范围内 f f f数组的最大值且范围在持续向右挪,所以想到用单调队列维护,则 f [ i ] = f [ q . f r o n t ( ) ] + v a l [ i ] f[i]=f[q.front()]+val[i] f[i]=f[q.front()]+val[i]
复杂度是 O ( n l o g n ) ,   A C O(nlog_n),\ AC O(nlogn), AC撒花
(本人是用双端队列写的单调队列 手写太难了
另外此题实现细节有亿点多 具体看代码
(这题可以对照Pogo的牛学习)

#include
using namespace std;
const int N=5e5+10;
int n;
long long f[N],d,k;
bool vis[N];
struct square
{
	long long x,val;
} a[N];
long long read()  //读入优化
{
	long long x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;  //我每次写读入优化都要忘这个 
} 
bool check(long long x)
{
	long long minn=max((long long)1,d-x),maxx=d+x;//行走步数的左右极值 
	memset(f,0x80,sizeof(f));
	memset(vis,true,sizeof(vis)); 
	f[0]=0;f[n+1]=999999999;
	
	deque<int> q;  //我习惯用双端队列来做单调队列
	int num=0;   //num表示下一个该入队的是num 
	for(int i=1;i<=n;++i)
	{	
		for( ;num<i;++num)//枚举i之前还未入队的元素
		{					   //把能够到达i(与i距离在minn以上)的元素的入队 
			if(a[i].x-a[num].x<minn) break;
			if(!vis[num]) continue;
			while(!q.empty()&&f[q.back()]<=f[num]) 
				q.pop_back();//队尾弹出f[]比j小的元素 
			q.push_back(num);
		}
		
		while(!q.empty()&&a[q.front()].x+maxx<a[i].x) 
			q.pop_front();   //队首弹出与i距离太远的元素 
			
		if(!q.empty()) f[i]=f[q.front()]+a[i].val;//更新f[i] 
		else vis[i]=false;
		
		if(f[i]>=k) return true;
	} 
	return false;
}
int main()
{
	n=read();d=read();k=read();
	for(int i=1;i<=n;++i)
	{
		a[i].x=read();
		a[i].val=read();
	}
	a[n+1].x=999999999;
	bool flag=false;   //flag用来判断是否能够获得k分 
	long long l=1,r=max(d,a[n].x);   //l,r是二分左右端点  
	while(l<r)
	{
		long long mid=(l+r)>>1;
		if(check(mid)) r=mid,flag=true;
		else l=mid+1;
	}
	if(flag) printf("%d",l);
	else printf("-1");
}

20.摆花

传送门
这题基本上是裸的多重背包,只不过统计的是所有的方案数,只需要把先前的方案数累加而非取 m a x max max就行了: f [ j ] = f [ j ] + f [ j − k ] f[j]=f[j]+f[j-k] f[j]=f[j]+f[jk]
数据范围还很小,你甚至不用二进制优化,直接 O ( n   m   k ) O(n\ m\ k) O(n m k)就过了

#include
using namespace std;
const int N=110,M=110,mod=1000007;
int n,m,a[N],f[M];
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;++i)
		cin>>a[i];
	f[0]=1;//边界值
	for(int i=1;i<=n;++i)
		for(int j=m;j>=1;--j)
			for(int k=1;k<=min(a[i],j);++k)  //直接多重背包朴素算法 
				f[j]=(f[j]+f[j-k])%mod;
	cout<<f[m];
}

21.摆渡车

传送门
傻子都知道第一步要排序
∗ * 那么我们先想想状态怎么转移,很容易想到可以,用 f [ i ] f[i] f[i]表示前 i i i个人等待最少时间:
                         f [ i ] = m i n ( f [ i ] , f [ j − 1 ]   +   j 至 i \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i]=min(f[i],f[j-1]\ +\ j至i                         f[i]=min(f[i],f[j1] + ji的总等待时间 ) ) )(上一班车最后一个走的是 j − 1 j-1 j1)
但是对于 j 至 i j至i ji的等待时间我们无法确定车在是什么时候到的:在 i i i来之前就已经到了? 还是 i i i来之后才到?那 i i i又等了多久?
所以必须还要枚举等待时间,但是 t [ i ] t[i] t[i]的取值范围有点吓人的啊,不可能直接当做循环
观察到每个人等待时间最多不超过m(若超过了则意味着在他开始等之后车来过而他却没上 这不是有毛病吗 )
那么我们可以想到枚举每个人等待的时间,这样最多也就 m m m次而已,时间复杂度完全足够
∗ * 那么我们就用 f [ i ] [ k ] f[i][k] f[i][k]表示前i个学生 且 第i个学生等待时间为k 的最小总等待时间 :
                         f [ i ] [ k ] = m i n ( f [ i ] [ k ] , f [ j ] [ t ]   +   j 至 i \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i][k]=min(f[i][k],f[j][t ]\ +\ j至i                         f[i][k]=min(f[i][k],f[j][t] + ji的总等待时间 ) ) )
四个字母怎么行,我们发现方程中,k与t是有等量关系的:
     j \ \ \ \ j     j等待时间为 t t t
⇒ \Rightarrow j j j所乘车发车时间 t [ j ] + t t[j]+t t[j]+t
⇒ \Rightarrow j j j所乘车下一班即 i i i所乘车到达时间 T = [ i ] + t + m T=[i]+t+m T=[i]+t+m
⇒ \Rightarrow i i i等待时间 k = m i n ( T − [ i ] , 0 ) k=mi n(T-[i],0) k=min(T[i],0)(车到达之后可能直接把 i i i接走,或是等 i i i来了才接走)
∗ * 那转移方程不就出来了:
                         f [ i ] [ m i n ( T − [ i ] , 0 ) ] = m i n ( f [ i ] [ m i n ( T − [ i ] , 0 ) ] , f [ j ] [ t ]   +   j 至 i \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i][mi n(T-[i],0)]=min(f[i][mi n(T-[i],0)],f[j][t ]\ +\ j至i                         f[i][min(T[i],0)]=min(f[i][min(T[i],0)],f[j][t] + ji的总等待时间 ) ) )
我们可以由 j j j的等待时间推出 i i i的等待时间,当然也可以由 i i i的等待时间推出 j j j的等待时间,就看你想要怎么推了

#include
using namespace std;
const int N=510,M=110;
int n,m;
long long f[N][M],t[N],sum[N];
//f[i][k]表示前i个学生 且 第i个学生等待时间为k 的最小总等待时间 
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;++i)
		cin>>t[i];
	sort(t+1,t+1+n);  //先将学生排序 
	for(int i=1;i<=n;++i)     
		sum[i]=sum[i-1]+t[i];  
  /*   预处理前i个学生的可以上车的时间,
 则等到t时刻,j~i的总等待时间=t*(i-j+1)-(sum[i]-sum[j-1])  */
 	memset(f,0x7f,sizeof(f));
 	for(int i=0;i<m;++i)
 		f[0][i]=0;//边界值 
	for(int i=1;i<=n;++i)
		for(int j=1;j<=i;++j) 
		{//学生j~i 在i可以上车之后共同上车
		    int maxx=min((long long)m,t[i]-t[j-1]);//此时j-1必须已经走了 
			for(int k=0;k<=maxx;++k)
			{//枚举学生j-1等待的时间(最多为m-1),可计算出此次发车的时间
			    int T; //T表示此次发车时刻
				if(j==1) T=max(t[j-1]+k,t[i]);//j-1=0时,是不用算上车回来的时间的 
				else T=max(t[j-1]+k+m,t[i]); 
				long long all=f[j-1][k]+T*(i-j+1)-(sum[i]-sum[j-1]);
				f[i][T-t[i]]=min(all,f[i][T-t[i]]);
			}
		}
	long long ans=999999999;
	for(int i=0;i<m;++i)
		ans=min(ans,f[n][i]);
	cout<<ans; 
}

22.POGO的牛

(Pogo-Cow S)
传送门
这题乍一看跟跳房子有点像,但是那道题每一步的距离是确定在某一范围内,而这道题是每一步都要大于等于上一步,想要直接枚举满足范围的步数就十分困难
那么我们就直接将 i i i的状态由所有的 j ( j < i ) j(jj(j<i)的状态转移而来,而不去枚举步数范围
我们就用 f [ i ] [ j ] f[i][j] f[i][j]表示第 i i i个点从 j j j跳过来的最优解,则:
f [ i ] [ j ] = m a x ( f [ j ] [ k ] ) + p [ i ] )    ( k < j < i ) f[i][j]=max(f[j][k])+p[i])\ \ (kf[i][j]=max(f[j][k])+p[i])  (k<j<i)
但是这样是三层循环 O ( n 3 ) O(n^3) O(n3),于是就炸了
再想想跳房子我们是用一个单调队列维护每一步可行范围内的最大值
而这道题我们要找的是 m a x ( f [ j ] [ k ] ) max(f[j][k]) max(f[j][k]),如果将 j j j定住 i i i向右移,则此时满足范围的&k&是向左移的
每次向左移我们就取一个 m a x max max,那我们就能直接得到 m a x ( f [ j ] [ k ] ) max(f[j][k]) max(f[j][k])
所以最外层循环我们得先放 j j j,定住 j j j后再向右移 i i i,并用一个指针去移 k k k,这样就把复杂度压到 O ( n 2 ) O(n^2) O(n2)
(另外,由于这道题可以往俩个方向走,所以需要两次dp,一次向前,一次向后)

#include
using namespace std;
const int N=1e3+10;
int n,f[N][N],ans;
struct position
{
	int x,p;
} a[N];
bool cmp(position x,position y)
{	return x.x<y.x; }
int main()
{
	cin>>n;
	for(int i=1;i<=n;++i)
		cin>>a[i].x>>a[i].p;
	sort(a+1,a+1+n,cmp);      //按照位置排序 
	//正着dp///////////////////////////////////////
	for(int i=1;i<=n;++i)
		f[i][i]=a[i].p;//边界值 
	for(int j=1;j<=n;++j)
	{
		int k=j,max_j_k=0;
		for(int i=j+1;i<=n;++i)
		{
			while(k&&(a[i].x-a[j].x>=a[j].x-a[k].x))  //如果k能往前移 
				max_j_k=max(max_j_k,f[j][k]),--k;
			f[i][j]=max_j_k+a[i].p; 
			ans=max(ans,f[i][j]);
		}
	}	
	//倒着dp///////////////////////////////////////
	for(int i=1;i<=n;++i)
		f[i][i]=a[i].p;//边界值 
	for(int j=n;j>=1;--j)
	{
		int k=j,max_j_k=0;
		for(int i=j-1;i>=1;--i)
		{
			while(k<=n&&(a[j].x-a[i].x>=a[k].x-a[j].x))  //如果k能往前移 
				max_j_k=max(max_j_k,f[j][k]),++k;
			f[i][j]=max_j_k+a[i].p;
			ans=max(ans,f[i][j]);
		}
	}	
	///////////////////////////////////////////////	
	cout<<ans;
}

23.乌龟棋

传送门
既然四种卡片的数量最多也就40张,那么我们直接简单的枚举4种卡片的数量就行了
这有点类似于背包问题,我们还需要枚举走到了哪里
所以转移方程: f [ i ] [ a ] [ b ] [ c ] [ d ] = m a x ( f [ p o s ] [ a ] [ b ] [ c ] [ d ] , f [ p o s − k ] [ a − 1 ] [ b ] [ c ] [ d ] . . . [ b − 1 ] . . . [ c − 1 ] . . . [ d − 1 ] ) f[i][a][b][c][d]=max(f[pos][a][b][c][d],f[pos-k][a-1][b][c][d]...[b-1]...[c-1]...[d-1]) f[i][a][b][c][d]=max(f[pos][a][b][c][d],f[posk][a1][b][c][d]...[b1]...[c1]...[d1])
但是这样不仅空间爆炸,时间也过不了
题目说卡牌的数量刚好满足走到终点,那走到的位置就可以直接使用的不同卡牌的数量来表示了: p o s = a + b ∗ 2 + c ∗ 3 + d ∗ 4 + 1 pos=a+b*2+c*3+d*4+1 pos=a+b2+c3+d4+1 (+1是因为从位置1出发)

#include
using namespace std;
const int N=500,M=200;
int n,m,val[N],cnt[5];
int f[50][50][50][50];
int find_pos(int a,int b,int c,int d)
{  return a+2*b+3*c+4*d; } 
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",&val[i]);
	for(int i=1;i<=m;++i)
	{
		int x; scanf("%d",&x);	
		cnt[x]++;          //记录不同卡牌的数量 
	}
	f[0][0][0][0]=val[1];  //边界值 
	for(int a=0;a<=cnt[1];++a)
		for(int b=0;b<=cnt[2];++b)
			for(int c=0;c<=cnt[3];++c)
				for(int d=0;d<=cnt[4];++d)
				{
					int pos=find_pos(a,b,c,d)+1;//因为刚开始就在起点1了,所以pos要+1 
					if(a) f[a][b][c][d]=max(f[a][b][c][d],f[a-1][b][c][d]+val[pos]);
              	    if(b) f[a][b][c][d]=max(f[a][b][c][d],f[a][b-1][c][d]+val[pos]);
                   	if(c) f[a][b][c][d]=max(f[a][b][c][d],f[a][b][c-1][d]+val[pos]);
                   	if(d) f[a][b][c][d]=max(f[a][b][c][d],f[a][b][c][d-1]+val[pos]);
				}
	int ans;
	ans=f[cnt[1]][cnt[2]][cnt[3]][cnt[4]];//所有卡牌用完时,即到达终点时,为答案 
	printf("%d",ans);
}

24.跑步

(Running S)
传送门
这题转移方程题目基本上都说的很明白了
需要注意的是由于若于某个时刻疲劳值恢复至 0 0 0,无法确定是什么时刻开始休息的
所以这道题我们采用由前面推至后面的方式,即所谓的填表,而不是刷表
f [ i ] [ j ] f[i][j] f[i][j]表示 i i i分钟,疲劳值为 j j j的最大跑步距离:
如果在第 i i i分钟内跑步,她可以在这一分钟内跑 d i d_i di 米,并且疲劳值 + 1 +1 +1
f [ i + 1 ] [ j + 1 ] = m a x ( f [ i + 1 ] [ j + 1 ] , f [ i ] [ j ] + d [ i + 1 ] ) f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1]) f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1])
如果选择休息,那么她的疲劳度就会每分钟减少 1 1 1,但她必须休息到疲劳度恢复到 0 0 0 为止:
f [ i + j ] [ 0 ] = m a x ( f [ i + j ] [ 0 ] , f [ i ] [ j ] ) f[i+j][0]=max(f[i+j][0],f[i][j]) f[i+j][0]=max(f[i+j][0],f[i][j])
在疲劳度为 0 0 0 时休息的话,疲劳度不会再变动:
f [ i + 1 ] [ j ] = m a x ( f [ i + 1 ] [ j ] , f [ i ] [ j ] )     ( j = 0 ) f[i+1][j]=max(f[i+1][j],f[i][j])\ \ \ (j=0) f[i+1][j]=max(f[i+1][j],f[i][j])   (j=0)
最后疲劳值恢复至 0 0 0,所以答案为 f [ n ] [ 0 ] f[n][0] f[n][0]

#include
using namespace std;
const int N=1e4+5e2+10,M=5e2+10; 
//注意N的范围,在第二个方程里面有一个f[i+j][0],必须加上500,否则会RE 
int n,m,d[N],f[N][M];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",&d[i]);
	for(int i=0;i<n;++i)
		for(int j=0;j<=min(m,i);++j)//i时刻疲劳值不可能超过i 
			{
				if(!j) f[i+1][0]=max(f[i+1][0],f[i][0]);  
			    //在i时刻休息且疲劳值已经为0,不再下降 
				else f[i+j][0]=max(f[i+j][0],f[i][j]);
				//在i时刻休息至疲劳值为0 
				if(j!=m) f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1]);
				//在i时刻此时疲劳值没满,可向前跑 
			}
	printf("%d",f[n][0]);//答案要求结束时疲劳值是0 
}

25.过河

传送门
这题转移方程太显然: f [ i ] = m a x ( f [ i ] , f [ i − k ] ) + 1 / 0 ) ( 取 决 于 i 是 否 有 石 子 ) f[i]=max(f[i],f[i−k])+1/0) (取决于i是否有石子) f[i]=max(f[i],f[ik])+1/0)(i)
但是 L ≤ 1 e 9 L\le 1e9 L1e9就很迷,不能直接枚举,而 m m m又才 100 100 100

于是我想到了用分块,原理很简单,但是调了我一晚上
欢迎博客欣赏(本来想在洛谷写篇题解的结果题解已经交满了)

而这里大众的做法是进行路径压缩,石子 i i i j j j的距离 d i s [ i ] = ( x [ i ] − x [ j ] ) m o d   l c m ( S . . . T ) dis[i]=(x[i]-x[j])mod\ lcm(S...T) dis[i]=(x[i]x[j])mod lcm(S...T)
(若不想求 l c m lcm lcm可直接 m o d   2520 = l c m ( 1 , 2 , . . . , 10 ) mod\ 2520=lcm(1,2,...,10) mod 2520=lcm(1,2,...,10))

#include
using namespace std;
int a[104];
int re[104];
int rock[1000005];
int f[1000005]; 
int L,s,t,m;
int main()
{
	cin>>L;
	cin>>s>>t>>m;
    for(int i=1;i<=m;i++)
    	cin>>a[i];
    sort(a+1,a+m+1); 
    for(int i=1;i<=m;i++)
        re[i]=(a[i]-a[i-1])%2520; //路径压缩
    for(int i=1;i<=m;i++)
        a[i]=a[i-1]+re[i],rock[a[i]]=1; //按照压缩后的lu
    L=a[m];
    for(int i=0;i<=L+t;i++)
    	f[i]=m;
    f[0]=0;
    for(int i=1;i<=L+t;i++)
        for(int j=s;j<=t;j++)
		{
            if(i-j>=0)
            	f[i]=min(f[i],f[i-j]);  
            f[i]+=rock[i];
        }
    int res=m;
	for(int i=L;i<L+t;i++)
		res=min(res,f[i]);
    cout<<res<<endl;
    return 0;
}

26.牛的词汇

(The Cow Lexicon S)
传送门
这题大概算是字符串题目,主要考查字符串处理,暴力dp就行了
f [ i ] f[i] f[i]表示母串前 i i i个最少需要删除的字母, c h e c k ( i , j ) check(i,j) check(i,j) i i i个若以 j j j为后缀需要删除的字母个数,则:
若 j 能 是 i 的 后 缀 : f [ i ] = m i n ( f [ i ] , f [ i − l e n [ j ] − c h e c k ( i , j ) ] + c h e c k ( i , j ) 若 不 能 是 i 的 后 缀 : f [ i ] = m i n ( f [ i ] , f [ i − 1 ] + 1 ) 若j能是i的后缀:f[i]=min(f[i],f[i-len[j]-check(i,j)]+check(i,j)\\ 若不能是i的后缀:f[i]=min(f[i],f[i-1]+1) jif[i]=min(f[i],f[ilen[j]check(i,j)]+check(i,j)if[i]=min(f[i],f[i1]+1)

#include
using namespace std;
int n,w,f[301];
char s[301],ch[601][26];
int check(int i,int j,int len)
{
	for(int q=len,k=i;k>=1;--k)
	{
		if(s[k]==ch[j][q]) q--; //每匹配一个字母则q-- 
		if(!q) return i-k+1-len;//若q为0,则表示j串已经匹配成功
	}
	return -1;
}
int main()
{
	scanf("%d%d%s",&n,&w,s+1);
	for(int i=1;i<=n;++i)
    	scanf("%s",ch[i]+1);
    memset(f,0x7f,sizeof(f));f[0]=0; //边界值 
	for(int i=1;i<=w;++i)
	{
		for(int j=1;j<=n;++j)
		{
			int len=strlen(ch[j]+1);
			if(i>=len&&s[i]==ch[j][len])//至少满足最后一个字母要相同 
			{
				int num=check(i,j,len);
				if(num!=-1) f[i]=min(f[i],f[i-len-num]+num);
			}
		}
		f[i]=min(f[i],f[i-1]+1);
	}
	printf("%d",f[w]);
}

…龟速更新中

你可能感兴趣的:(DP练习)