DP练习
马上要联赛了,我又要来抱一抱DP的佛脚\(QWQ\)
最近膜你赛的题目的常规\(dp\)都不是很难推,但是优化这一方面确实不是很好,所以我来这里复(学)习一下一些常见DP优化和其他类型的DP,以及一些简单的dp(dalao勿D)qwq (未完待续)
9、P2904 [USACO08MAR]跨河River Crossing(dp)
1、P2059 [JLOI2013]卡牌游戏
这道题也是一道比较基础的 (算是) 概率dp吧。
我们首先可以来分析一下这道题目的状态,我们要明白这道题的操作流程,如果你不是很懂的话请自行那一副牌来模拟一下。
我们现在来看一下这个\(dp\)的状态转移的方程。
\(f[i][j]\)表示还剩\(i\)个人的时候庄家向后数\(j\)位的时候的人获胜的概率。
所以,我们现在来考虑一下我们\(f[i][]\)的状态一定是从\(f[i-1][]\)转移来,所以我们现在就来讨论一下\(j\)的情况。
这个时候我们可以来枚举一下当\(j\)为庄家是所抽的牌为\(k\)时的情况。这时我们可以假设此时淘汰的人为\(w\),如果\(w==j\)那么这个情况就不用考虑了, (自己的没了,还玩个锤子啊)
如果,\(w<j\)的时候,这时候\(j\)的位置就变成了\(j-c\)
如果,\(w>j\)的时候,\(j\)的位置就变成了\(i-w+j\)
所以,我们的状态转移方程就可以写出来了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
#define db double
const int N=1e5+7;
db f[1005][1005];//f[i][j]表示在还剩i个人的时候从庄家开始数的第j个人获胜的概率
itn a[N],n,m;
int main(){
scan(n);scan(m);
for(itn i=1;i<=m;i++){
scan(a[i]);
}
f[1][1]=1.0;
for(itn i=2;i<=n;i++){
for(int j=1;j<=i;j++){
for(itn k=1;k<=m;k++){
itn w;
if(a[k]%i==0){
w=i;
}else{
w=a[k]%i;
}
if(w>j){
f[i][j]+=f[i-1][i+j-w]/m;
}else{
f[i][j]+=f[i-1][j-w]/m;
}
}
}
}
for(int i=1;i<=n;i++){
printf("%.2lf%% ",f[n][i]*100.0);
}
return 0;
}
2、P1850 换教室(期望DP)
这道题是一道期望DP的题,但是应该难度还不算太大。其实就是将计算期望的方法融入到DP式子中。
首先,我们可以来分析一下这道题,我们现在已知的是每一天的原来\(c_i\)和现在的教室\(d_i\),以及要到这个教室的路径贡献\(dis[i][j]\)(可以提前预处理出来),还有就是我们每一次换教室成功的概率\(k_i\),我们就可以枚举每一次的换教室的选还是不选,然后再通过概率去进行计算期望。这个时候,我们的基本状态就可以设计出来了。
\(f[i][j][0/1]\)表示在\(i\)之前的时间中有\(j\)个教室进行了交换,\(0/1\)表示这个教室选择换还是不换的期望最小值。
现在我们来考虑如何进行转移。
当\(f[i][j][0]\)时,表示这个教室我们不选择进行交换,我们来考虑一下上一个时间的状态
当上一个时间不选择交换时,贡献
当上一个时间选择交换时,贡献
同理,我们来考虑一下\(f[i][j][1]\)选择这个教室进行交换的状态
当上一个时间不选择交换时,贡献
当上一个时间选择交换时,贡献
所以我们就可以把这道题解决掉了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
#define db double
const int N=2007;
const int inf=1e9+7;
int d[N],c[N],n,m,v,e,dis[307][307];
db f[N][N][2],k[N],ans;
int main(){
scan(n);scan(m);scan(v);scan(e);
for(int i=1;i<=n;i++){
scan(c[i]);
}
for(int i=1;i<=n;i++){
scan(d[i]);
}
for(int i=1;i<=n;i++){
scanf("%lf",&k[i]);
}
for(int i=1;i<=v;i++){
for(int j=1;j<i;j++){
dis[i][j]=dis[j][i]=inf;
}
}
for(itn i=1;i<=e;i++){
itn a,b,w;
scan(a);scan(b);scan(w);
dis[a][b]=min(w,dis[a][b]);
dis[b][a]=dis[a][b];
}
for(int s=1;s<=v;s++){
for(int i=1;i<=v;i++){
for(int j=1;j<i;j++){
dis[i][j]=min(dis[i][j],dis[i][s]+dis[s][j]);
dis[j][i]=dis[i][j];
}
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j][0]=f[i][j][1]=inf;
}
}
f[1][0][0]=0;
f[1][1][1]=0;
for(itn i=2;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j][0]=min(f[i-1][j][1]+k[i-1]*dis[d[i-1]][c[i]]+(1-k[i-1])*dis[c[i-1]][c[i]],f[i-1][j][0]+1.0*dis[c[i-1]][c[i]]);
if(j>=1){
f[i][j][1]=min(f[i-1][j-1][1]+k[i-1]*k[i]*dis[d[i-1]][d[i]]+(1-k[i-1])*k[i]*dis[c[i-1]][d[i]]+k[i-1]*(1-k[i])*dis[d[i-1]][c[i]]+(1-k[i-1])*(1-k[i])*dis[c[i-1]][c[i]],f[i-1][j-1][0]+k[i]*dis[c[i-1]][d[i]]+(1-k[i])*dis[c[i-1]][c[i]]);
}
}
}
ans=inf;
for(int i=0;i<=m;i++){
for(int j=0;j<=1;j++){
ans=min(ans,f[n][i][j]);
}
}
printf("%.2lf",ans);
return 0;
}
3、P2157 [SDOI2009]学校食堂(状压DP)
题目描述
小F 的学校在城市的一个偏僻角落,所有学生都只好在学校吃饭。学校有一个食堂,虽然简陋,但食堂大厨总能做出让同学们满意的菜肴。当然,不同的人口味也不一定相同,但每个人的口味都可以用一个非负整数表示。 由于人手不够,食堂每次只能为一个人做菜。做每道菜所需的时间是和前一道菜有关的,若前一道菜的对应的口味是a,这一道为b,则做这道菜所需的时间为(a or b)-(a and b),而做第一道菜是不需要计算时间的。其中,or 和and 表示整数逐位或运算及逐位与运算,C语言中对应的运算符为“|”和“&”。
学生数目相对于这个学校还是比较多的,吃饭做菜往往就会花去不少时间。因此,学校食堂偶尔会不按照大家的排队顺序做菜,以缩短总的进餐时间。
虽然同学们能够理解学校食堂的这种做法,不过每个同学还是有一定容忍度的。也就是说,队伍中的第i 个同学,最多允许紧跟他身后的Bi 个人先拿到饭菜。一旦在此之后的任意同学比当前同学先拿到饭,当前同学将会十分愤怒。因此,食堂做菜还得照顾到同学们的情绪。 现在,小F 想知道在满足所有人的容忍度这一前提下,自己的学校食堂做完这些菜最少需要多少时间。
输入格式
第一行包含一个正整数C,表示测试点的数据组数。 每组数据的第一行包含一个正整数N,表示同学数。 每组数据的第二行起共N行,每行包含两个用空格分隔的非负整数Ti和Bi,表示按队伍顺序从前往后的每个同学所需的菜的口味和这个同学的忍受度。 每组数据之间没有多余空行。
输出格式
包含C行,每行一个整数,表示对应数据中食堂完成所有菜所需的最少时间。
这道题算是一道比较毒瘤的状压dp题,第一开始我自己设计时,状态少了一维,然后发现无法转移,就自闭的去看了看题解,(真的好毒瘤)。但是,不得不说这道题的思路还是很毒瘤巧妙。
我们现在先设计出\(f[i][j][k]\)表示在前\(i-1\)个人已经买过饭了,第\(i\)个人以及其后面的7个人打饭的状态(因为题目中数据范围给到了7),\(k\)则表示最后一个人打饭的编号(这里我们要注意,因为\(-8 \leq k \leq 7\),所以,我们在存的时候要注意+8)。
下面,我们来考虑一下我们转移过程。
\(if(j\&1)\)为真时,表示第\(i\)点已经打过饭了,我们就可以将状态进行转移,
\(if(j\&1)\)为假时,我们是没有办法来转移\(i+1\),所以,我么需要去枚举\(h\)从0-7,
在转移的过程中,我们还需要去判断一下是否越界。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
#define ll long long
const int N=1007;
const int inf=0x3f3f3f3f;
int f[N][1<<8][20];
int T,n;
int b[N],t[N];
int main(){
scan(T);
while(T--){
scan(n);
memset(t,0,sizeof(t));
memset(b,0,sizeof(b));
for(itn i=1;i<=n;i++){
scan(t[i]);scan(b[i]);
}
memset(f,inf,sizeof(f));//初始化
f[1][0][7]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<8);j++){
for(int k=-8;k<=7;k++){
if(f[i][j][k+8]!=inf){
if(j&1){
f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);//直接转移
}else{
int lim=inf;
for(int h=0;h<=7;h++){
if(!((j>>h)&1)){
if(i+h>lim)break;
lim=min(lim,i+h+b[i+h]);//枚举第几个人吃饭,更新范围值
int s;
if(i+k<=0){
s=0;
}else{
s=t[i+h]^t[i+k];
}
f[i][j|(1<<h)][h+8]=min(f[i][j|(1<<h)][h+8],f[i][j][k+8]+s);//转移答案
}
}
}
}
}
}
}
int ans=inf;
for(int i=0;i<=8;i++){
ans=min(ans,f[n+1][0][i]);//统计答案
}
printf("%d\n",ans);
}
return 0;
}
4、P3399 丝绸之路(简单dp)
题目描述
题目描述
小仓鼠带着货物,从中国送到安息,丝绸之路包括起点和终点一共有N+1个城市,0号城市是起点长安,N号城市是终点巴格达。要求不超过M天内必须到达终点。一天的时间可以从一个城市到连续的下一个城市。从i-1城市到i城市距离是Di。
大家都知道,连续赶路是很辛苦的,所以小仓鼠可以在一个城市时,可以有以下选择:
移动:向下一个城市进发
休息:呆在原来的城市不动
沙漠天气变化无常,在天气很不好时,前进会遇到很多困难。我们把M天的第j(1<=j<=M)天的气候恶劣值记为Cj。从i-1城市移动到i城市在第j天进发时,需要耗费Di*Cj的疲劳度。
不过小仓鼠还是有选择权的,可以避开比较恶劣的天气,休息是不会消耗疲劳值的。现在他想知道整个行程最少要消耗多少疲劳值。
输入格式
第一行2个整数N,M
连续N行每行一个整数Dj
连续M行每行一个整数Cj
输出格式
一个整数,表示最小疲劳度
这道题是一道很简单的普通\(dp\)题,我们可以很简单的设计出基本状态\(f[i][j]\)表示,我们在第\(i\)天时所在的\(j\)城市。根据题目要求,我们就很简单的推出来了转移方程。
最后一点,要注意数组的初始化。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
#define ll long long
const int N=1e3+7;
int f[N][N],n,m,d[N],c[N];
int main(){
scan(n);scan(m);
for(int i=1;i<=n;i++){
scan(d[i]);
}
for(int j=1;j<=m;j++){
scan(c[j]);
}
memset(f,63,sizeof(f));
for(int i=0;i<=m;i++){
f[i][0]=0;//数组的初始化
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
f[i][j]=min(f[i-1][j],f[i-1][j-1]+c[i]*d[j]);
}
}
itn ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,f[m][i]);
}
printf("%d\n",ans);
return 0;
}
5、P1435 回文字串(简单dp)
题目描述
题目描述
回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。
比如 “Ab3bd”插入2个字符后可以变成回文词“dAb3bAd”或“Adb3bdA”,但是插入少于2个的字符无法变成回文词。
注:此问题区分大小写
输入格式
一个字符串(0< strlen <=1000)
输出格式
有且只有一个整数,即最少插入字符数
这道题,是一道十分巧妙的题目,虽然说
——by yxd
所以,我们可以想一下回文子串的基本性质,就是正反着读都是相同的,所以我们可以思路转化一下,去判断同一个串,正反的\(lcs\),再拿总长度减去,就可以得到最后的答案。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
const int N=1007;
char a[N],b[N];
itn f[N][N];
int main(){
scanf("%s",b+1);
itn n=strlen(b+1);
for(int i=1;i<=n;i++){
a[i]=b[n-i+1];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]=max(f[i][j],max(f[i-1][j],f[i][j-1]));
if(a[i]==b[j]){
f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
}
}
printf("%d\n",n-f[n][n]);
return 0;
}
6、P1077 摆花(类背包dp)
题目描述
题目描述
小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共m盆。通过调查顾客的喜好,小明列出了顾客最喜欢的n种花,从1到n标号。为了在门口展出更多种花,规定第i种花不能超过ai盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。
试编程计算,一共有多少种不同的摆花方案。
输入格式
第一行包含两个正整数n和m,中间用一个空格隔开。
第二行有n个整数,每两个整数之间用一个空格隔开,依次表示a1,a2,…,an。
输出格式
一个整数,表示有多少种方案。注意:因为方案数可能很多,请输出方案数对1000007取模的结果。
这道题,其实可以属于一道背包\(dp\),不难发现,这道题的数据范围是可以支持我们进行三次方的转移的。所以,我们就可以设出状态\(f[i][j]\)表示,第\(i\)种花,我们放在j的位置。这样,我们的方程就可以推了出来。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
const int N=107;
int f[N][N];
int n,m,a[N];
const int mod=1000007;
int main(){
scan(n);scan(m);
for(int i=1;i<=n;i++){
scan(a[i]);
}
f[0][0]=1;
for(itn i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=min(j,a[i]);k++){
f[i][j]=(f[i][j]+f[i-1][j-k])%mod;
}
}
}
printf("%d\n",f[n][m]%mod);
return 0;
}
7、P1736 创意吃鱼法(悬线思想+dp)
题目描述
回到家中的猫猫把三桶鱼全部转移到了她那长方形大池子中,然后开始思考:到底要以何种方法吃鱼呢(猫猫就是这么可爱,吃鱼也要想好吃法 ^_*)。她发现,把大池子视为01矩阵(0表示对应位置无鱼,1表示对应位置有鱼)有助于决定吃鱼策略。
在代表池子的01矩阵中,有很多的正方形子矩阵,如果某个正方形子矩阵的某条对角线上都有鱼,且此正方形子矩阵的其他地方无鱼,猫猫就可以从这个正方形子矩阵“对角线的一端”下口,只一吸,就能把对角线上的那一队鲜鱼吸入口中。
猫猫是个贪婪的家伙,所以她想一口吃掉尽量多的鱼。请你帮猫猫计算一下,她一口下去,最多可以吃掉多少条鱼?
输入格式
有多组输入数据,每组数据:
第一行有两个整数n和m(n,m≥1),描述池塘规模。接下来的n行,每行有m个数字(非“0”即“1”)。每两个数字之间用空格隔开。
对于30%的数据,有n,m≤100
对于60%的数据,有n,m≤1000
对于100%的数据,有n,m≤2500
输出格式
只有一个整数——猫猫一口下去可以吃掉的鱼的数量,占一行,行末有回车。
这道题的思想其实不是很难,我们需要去维护对角线,但是我们有没有办法直接去维护一个对角线。所以,我们可以转化一下,分别设出两个状态,一个去维护行的0的个数,一个去维护列的0的个数。我们在转移的时候就可以直接判断一下行列的0的个数是否符合,直接进行转移了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
const int N=2507;
int f[N][N],s1[N][N],s2[N][N],n,m,a[N][N];
itn ans=0;
int main(){
scan(n);scan(m);
for(int i=1;i<=n;i++){//左上-右下
for(int j=1;j<=m;j++){
scan(a[i][j]);
if(a[i][j]==0){
s1[i][j]=s1[i][j-1]+1;//维护行
s2[i][j]=s2[i-1][j]+1;//维护列
}else{
f[i][j]=min(f[i-1][j-1],min(s1[i][j-1],s2[i-1][j]))+1;//转移
ans=max(ans,f[i][j]);
}
}
}
memset(f,0,sizeof(f));//清空
memset(s1,0,sizeof(s1));
memset(s2,0,sizeof(s2));
for(int i=1;i<=n;i++){
for(int j=m;j>=1;j--){//右上-左下
if(a[i][j]==0){
s1[i][j]=s1[i][j+1]+1;
s2[i][j]=s2[i-1][j]+1;
}else{
f[i][j]=min(f[i-1][j+1],min(s1[i][j+1],s2[i-1][j]))+1;
ans=max(ans,f[i][j]);
}
}
}
printf("%d\n",ans);
return 0;
}
8、P2569 [SCOI2010]股票交易(单调队列优化DP),详情请点击进入单调队列专题
9、P2904 [USACO08MAR]跨河River Crossing(dp)
题目描述
Farmer John以及他的N(1 <= N <= 2,500)头奶牛打算过一条河,但他们所有的渡河工具,仅仅是一个木筏。 由于奶牛不会划船,在整个渡河过程中,FJ必须始终在木筏上。在这个基础上,木筏上的奶牛数目每增加1,FJ把木筏划到对岸就得花更多的时间。 当FJ一个人坐在木筏上,他把木筏划到对岸需要M(1 <= M <= 1000)分钟。当木筏搭载的奶牛数目从i-1增加到i时,FJ得多花M_i(1 <= M_i <= 1000)分钟才能把木筏划过河(也就是说,船上有1头奶牛时,FJ得花M+M_1分钟渡河;船上有2头奶牛时,时间就变成M+M_1+M_2分钟。后面的依此类推)。那么,FJ最少要花多少时间,才能把所有奶牛带到对岸呢?当然,这个时间得包括FJ一个人把木筏从对岸划回来接下一批的奶牛的时间。
输入格式
Line 1: Two space-separated integers: N and M
Lines 2..N+1: Line i+1 contains a single integer: M_i
输出格式
Line 1: The minimum time it takes for Farmer John to get all of the cows across the river.
这道题我们可以使用背包的思想来进行处理,我们设出状态\(f[i]\)表示,在运过去\(i\)头牛是我们所需要的最短的时间。然后,我们在用一个前缀和来维护一下\(a_i\)。所以,我们就可以把式子推出来了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
template<typename type>
void scan(type &x){
type f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
#define itn int
#define ll long long
const int N=2507;
ll sum[N],a[N],n,m;
ll f[N];
int main(){
scan(n);scan(m);
sum[0]=2*m;//加上每一次来回所需要的时间
for(int i=1;i<=n;i++){
scan(a[i]);
sum[i]=sum[i-1]+a[i];//前缀和
}
for(itn i=1;i<=n;i++){
f[i]=sum[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
f[i]=min(f[i],f[j]+sum[i-j]);
}
}
printf("%lld\n",f[n]-m);
return 0;
}