dp总结

背包dp

01背包

特点:每个物品只有一个,两种情况选或不选
原始的状态转移方程:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+c[i])
表示到第i个物品当在容量为j时的最大价值,j-v[i]确保大于零表示在
前一个状态下再放入第i个物品能装下,在能放入的前提下加上i物品的
价值,否则由i-1状态转移来

优化后的状态转移方程:可以省略一维,因为每次更新i只是从i-1和i
中取最大的一个,取完可以直接覆盖和上一层就没有关系了,循环中第
一层是枚举物品种类,第二层循环从v开始倒着往前循环,因为它要取i
-1中f[j]和f[j-v[i]]+c[i]两个状态的最大,所以不能先覆盖前面的i-
1状态,而循环只需循环到j>=v[i]即可,若j-v[i]<0那就无法装下

for(int i=1;i<=n;i++)//n是物品种类,每种只有一个 
    for(int j=m;j>=v[i];j++)//m是背包容量  
	f[j]=max(f[j],f[j-v[i]]+c[i]);//不断覆盖上一层状态 

完全背包

特点:每种都有无数个,可以任意取,但其实第i种物品最多只能有
m/v[i]个

for(int i=1;i<=n;i++)//注释同上
	for(int j=v[i];j<=m;j++)
		f[j]=max(f[j],f[j-v[i]]+c[i]);

第二层要正序循环,因为每次都要算的是本层从v[i]到m中物品最多
能装几个,当循环到某个容量是多出来的容量恰好又能装一个物品,
这样把每层都算完再从下一层开始,就不用循环第三层物品个数了

多重背包

给出每种物品件数

在输入时可以判一下件数,若m/v[i]<件数,
件数=m/v[i],多的也装不下
其他与完全背包相似,但需加一层循环件数,因为可能物品件数并不
够单独装满背包

for(int i=1;i<=n;i++)
	for(int j=1;j<=s[i];j++)
		for(int k=m;k>=v[i];k--)
			f[k]=max(f[k],f[k-v[i]]+w[i]);
//优化:件数太多?可以用二进制拆分
for(int i=1;i<=n;i++)
{
	scanf("%d%d%d",&vi,&wi,&si);
	if(si>m/vi)si=m/vi;
	for(int j=1;j<=si;j<<=1)
	{
		v[++cnt]=j*vi;
		w[cnt]=j*wi;
		si-=j;
	}
	if(si>0)
	{
		v[++cnt]=si*vi;
		w[cnt]=si*wi;
	}
}
//二进制拆分后都存一起,省掉了一层循环 
//循环同01背包 

混合背包

其实就是变相的多重背包,只不过需要输入时预处理一下物品件数若
m/v[i]<件数,件数=m/v[i]
循环同混合背包优化前

分组背包

组中物品相互冲突最多选一件,也可不选

//因为物品有组的区分,所以要分别存
//用一个二维数组存g[i][j]存第i组第j个物品的编号1~n
//g[i][0]存下本组有多少物品
scanf("%d%d%d",&v,&n,&t);//v容量n物品数t组数 
for(int i=1;i<=n;i++)
{
	scanf("%d%d%d",&w[i],&c[i],&x);
	g[x][++g[x][0]]=i;
}
for(int i=1;i<=t;i++)//先循环组数 
	for(int j=v;j>=0;j--)//再循环背包容量 
		for(int k=1;k<=g[i][0];k++)//枚举第i组物品 
		{
			x=g[i][k];
			if(j>=w[g[i][k]])//此时要判大小 
			{
				f[j]=max(f[j],f[j-w[x]]+c[x]);
			}
		}
//最后输出f[m]即可 

线性dp

求最长上升子序列

题目描述:给出一串数求出最长上升序列长度并输出此序列
首先定义一个数存最大序列中的数的个数 ans=1因为最少也
会有一个不可能没有
因为没有给出有多少个数,用while输入 存下有多少个数

int x;
while(scanf("%d",&x)!=EOF)a[++n]=x;//a[i]数组存序列 
for(int i=1;i<=n;i++)//枚举每个数 
{
	f[i]=1;//都置为1最少1个 
	for(int j=1;j<i;j++)//从开头循环对比i和j 
	{
		if(a[i]>a[j]&&f[i]<f[j]+1)//若第i个比j大并且
		//f[i]要小于f[j]+1时用f[j]+1覆盖 
		{
			f[i]=f[j]+1;
			p[i]=j;//存指针第i个输出后指向第j个 
			if(ans<f[i]) 
			{
				ans=f[i];//存下最长序列 
				t=i;//存下最长序列最后一个数 
			}
		}
	}
}
//递归输出
//因为存的是最后一个,需要递归去找前一个数 
void out(int x)
{
	if(x==0)return;
	out(p[x]);
	printf("%d ",a[x]);
}

最长公共子串长度

//涉及到字符串比较用strncmp
//不断枚举 len1,len2
for(int i=0;i<len1;i++)
	for(int j=0;j<len2;j++)
		for(int k=1;k<=min(len1-i,len2-j);k++)//从1枚举 
		//每次比较k个字符从i,j开始比较 
			if(strncmp(s1+i,s2+j,k)==0)
				tot=max(tot,k);

区间dp

类似于将某种东西进行合并求最大值

石子合并<1>

简单模板
题目描述:n堆石子,每次选相邻两堆合并,最后剩一堆,每次得
分为合并后石子数,计算最小和最大得分

//用一维数组存每堆石子个数 
//用k枚举决策点,可在每个决策点应该加的数如何表示?-前缀和 
//在输入时预处理一下,求出前i堆石子个数 
 for(int i=1;i<=n;i++)
{
	scanf("%d",&a[i]);
	s[i]=s[i-1]+a[i];
}
//用f数组存最小g存最大 
//注意最小值f数组应置为极大值而不是零 
//而f[i][i]为0 
for(int len=2;len<=n;len++)//从长度为2开始,1堆不能进行合并 
{
	for(int i=1;i+len-1<=n;i++)//从i开始长度为len不能超过总堆数  
	{
		int j=i+len-1;
		for(int k=i;k<j;k++)//枚举决策点从i到j 
		{
			f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
			g[i][j]=max(g[i][j],g[i][k]+g[k+1][j]+s[j]-s[i-1]);
		}+s[j]-s[i-1]//是从i到j石子数前缀和相减 
	}
}

石子合并<2>

与1不同的是石子围成环,第1堆和最后一堆也相邻
此时可以将数组开成两倍将环拆开排两条链

for(int i=1;i<=n;i++)
{
	scanf("%d",&a[i]);
	a[n+i]=a[i];
}
for(int i=1;i<=2*n;i++)
{
	s[i]=s[i-1]+a[i];
}
//dp部分和1同 
//最后加一个循环求出从哪一堆开始时值最大/最小就好了  
int ans1=0x3f3f3f3f,ans2=0;
for(int i=1;i<=n;i++)
{
	ans1=min(ans1,f[i][n+i-1]);
	ans2=max(ans2,g[i][n+i-1]);
}

石子合并<3>

此题数据范围为1~2000,显然三层循环肯定会超时,怎么办?
此题巧妙之处在于它只让求最大值,那么一定有特殊方法 :
石子断开处只能为i+1或j-1可以减少一层循环k!
状态转移方程
g[i][j]=max(g[i+1][j],g[i][j-1])+s[j]-s[i-1]
要么在i+1,要么在j-1

低价回文

题目描述
给出一个字符串,将它变成回文,给出删去和添加某个字符的价格
求最小价格
此题乍一看可能会懵,反正我是懵了后来讲完才明白
首先是一个字符又能加又能删,这怎么办呢
其实要构成回文不管是删还是加都是一样的
只将删和加中的最小价格存下就行了
其他略

凸多边形的三角剖分

可能不好理解可参照矩阵连乘来做,矩阵连乘与石子合并类似
可以帮助理解本题

坐标dp

传纸条

题目描述:两个人在矩阵左上角和右下角,他们需要别人帮助他们
传纸条交流,纸条还要传递回去但不能经过同一个人(一个人只能
帮助传递一次)每个人给出一个好心程度,求参与传递的人的好心
程度的最大值
方法一:可以认为两个纸条都从左上角到右下角开四维数组存纸条
位置
动态转移方程
f[i1][j1][i2][j2]=max({f[i1-1][j1][i2-1][j2], f[i1-1][j1][i2][j2-1], f[i1][j1-1][i2-1][j2], f[i1][j1-1][i2][j2-1]}) +a[i1][j1]+a[i2][j2];
分别循环i1,j1,i2,j2分清横纵坐标!!!
i1==i2&&j1=j2跳过不可以一个人传递两次
最后输出f[m][n-1][m-1][n]f[m-1][n][m][n-1]即可
不可以输出f[m][n][m][n]此时i1==i2,j1==j2值为零
方法二:纸条要走m+n-1步用三维数组k存走了多少步
i1为1的横坐标i2为2的横坐标
状态转移方程
f[k][i1][i2]=max({f[k-1][i1-1][i2-1],f[k-1][i1-1][i2],f[k-1][i1][i2-1],f[k-1][i1][i2]})+a[i1][j1]+a[i2][j2];
计算出j1=k+1-i1,j2=k+1-i2

for(int k=2;k<=n+m-1;k++)//枚举步数 
{
	for(int i1=1;i1<=n;i1++)
	{
		for(int i2=1;i2<=n;i2++)
		{
			int j1=k+1-i1,j2=k+1-i2;
			if(j1<1||j2<1) continue;//保证坐标大于零 
			if(i1==i2) continue;//相等跳过 
			f[k][i1][i2]=max({f[k-1][i1-1][i2-1],f[k-1][i1-1][i2],f[k-1][i1][i2-1],f[k-1][i1][i2]})+a[i1][j1]+a[i2][j2];
		}
	}
}
printf("%d",f[n+m-2][n-1][n]);

矩阵取数

题目描述 :给出nm的矩阵 每次取出每行的第一个或最后一个数
第i次取数得分为取出数的和
2^i求最大得分
题目分析
此题行与行之间互不干涉,可以分开来求每行的最大得分再相加
此题数据较大long long无法存储
引入一个新知识__int128 39位数
用快读快写读入输出,可直接进行四则运算,比高精简单的多

__int128 a[105],f[105][105],ans;//直接定义__int128型数据 
//快读 
__int128 read()
{
	__int128 res=0;
	char scan[50];
	scanf(" %s",scan);
	for(int i=0;i<strlen(scan);i++)
	{
		res*=10;
		res+=scan[i]-'0';
	}
	return res;
}
//快写 
void print(__int128 num)
{
	if(num>9)
	{
		print(num/10);
	}
	putchar(num%10+'0');
}

for(int j=1;j<=m;j++)//重复读入计算完一行可以直接覆盖 
{
	memset(f,0,sizeof(f));//置零 
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		f[i][i]=a[i];//重复使用a,f数组 
	}
	for(int i=0;i<=n;i++)
	{
		for(int j=1;i+j<=n;j++)
		{
			f[j][i+j]=max(2*f[j+1][j+i]+2*a[j],2*f[j][j+i-1]+2*a[i+j]);
		}
	}
	ans+=f[1][n];//将每行最大相加再输出 
}

晴天小猪历险记之Hill

题目描述:给出一座山和爬每段路所用时间从左下角出发到山顶
用时最少为多少 ( 可以左右走或向左上方或右上方走)
注意山是锥形从本层第一个可以直接绕道最后一个或上一层最后一个

//因为求最小所以置为极大值 
f[n][1]=0;
for(int i=n;i>=1;i--)//从最后一层开始走 
{
	for(int j=1;j<=i;j++)//遍历该层每一段 
	{
		if(j==1)//在第一段 
		{
			f[i][j]=min({f[i][j],
						 f[i+1][j]+m[i+1][j],
						 f[i+1][j+1]+m[i+1][j+1],
						 f[i+1][i+1]+m[i+1][i+1]});
		}
		else if(j==i)//在最后一段 
		{
			f[i][j]=min({f[i][j],
						 f[i+1][j]+m[i+1][j],
						 f[i+1][j+1]+m[i+1][j+1],
						 f[i+1][1]+m[i+1][1]});
		}
		else//在中间 
		{
			f[i][j]=min({f[i][j],
						 f[i+1][j+1]+m[i+1][j+1],
						 f[i+1][j]+m[i+1][j]});
		}
	}
	f[i][1]=min(f[i][1],f[i][i]+m[i][i]);
	f[i][i]=min(f[i][i],f[i][1]+m[i][1]);
	for(int j=2;j<=i;j++)
	{
		f[i][j]=min(f[i][j],f[i][j-1]+m[i][j-1]);
	}
	for(int j=i-1;j>=1;j--)
	{
		f[i][j]=min(f[i][j],f[i][j+1]+m[i][j+1]);
	}
}
printf("%d",f[1][1]+m[1][1]);

方格取数

和传纸条差不多但是可以走同一个格子,只加一次就好了

int maxn=max({f[i1-1][j1][i2-1][j2],
			  f[i1][j1-1][i2-1][j2],
			  f[i1][j1-1][i2][j2-1],
			  f[i1-1][j1][i2][j2-1]});
if(i1==i2&&j1==j2)f[i1][j1][i2][j2]=maxn+a[i1][j1];
else f[i1][j1][i2][j2]=maxn+a[i1][j1]+a[i2][j2];
printf("%d",f[n][n][n][n]);

免费馅饼

思路大概就是存下每个时刻掉到舞台上的馅饼的价值
注意若不是恰好掉落不能接到,直接忽略即可
h-1%v[i]==0才行

三角蛋糕

找出最大的没有被老鼠咬坏的正三角形
注意三角尖的方向
特殊样例
1000000
00001
000
0

scanf("%d",&n);
for(int i=n,k=1;i>=1;i--,k++)
{
	for(int j=k;j<=n*2-k;j++)
	{
		scanf(" %c",&s[i][j]);
		if(s[i][j]=='0')
		{
			f[i][j]=g[i][j]=1;
			ans=1;
		}
	}
}
for(int i=1,k=n;i<=n;i++,k--)//正着搜尖朝下的 
{
	for(int j=k+1;j<=2*n-k;j+=2)//搜本行的所有三角形 
	{
		if(f[i][j]&&f[i-1][j-1]&&f[i-1][j]&&f[i-1][j+1])
		{
			f[i][j]=min(f[i-1][j-1],f[i-1][j+1])+1;
		}
		ans=max(ans,f[i][j]);
	}
}
for(int i=n,k=1;i>=1;i--,k++)//倒着搜尖朝上的 
{
	for(int j=k;j<=2*n-k;j+=2)
	{
		if(g[i][j]&&g[i+1][j-1]&&g[i+1][j]&&g[i+1][j+1])
		{
			g[i][j]=min(g[i+1][j-1],g[i+1][j+1])+1;
		}
		ans=max(ans,g[i][j]);
	}
}
ans*=ans;//三角形个数是最大的高的平方 
printf("%d",ans);

树形dp

最重要的是要建树!!!

没有上司的舞会

Ural 大学有 N 个职员,编号为 1~N。他们有从属关系,
也就是说他们的关系就像一棵以校长为根的树,父结点
就是子结点的直接上司。每个职员有一个快乐指数。现
在有个周年庆宴会,要求与会职员的快乐指数最大。但
是,没有职员愿和直接上司一起与会。

//此题可以考虑建树 
void addedge(int x,int y)//建树操作 
{
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
}
//深搜 
void dfs(int x)
{
	for(int i=h[x];i;i=nxt[i])
	{
		int y=to[i];
		dfs(y);
		f[x][0]+=max(f[y][0],f[y][1]);
		f[x][1]+=f[y][0];
	}
}
//加边操作 
while(scanf("%d%d",&x,&y))
{
	if(x==0&&y==0)break;
	addedge(y,x);
	in[x]++;//记录入度,入度为零的是树根  
}
dfs(root);//从树根开始搜 
printf("%d",max(f[root][0],f[root][1]));//从选树根和不选中取最大 

小胖守皇宫

//加边略 
void dfs(int now,int fa)
{
	int mi=0x3f3f3f3f;//求最小置为极大 
	for(int i=h[now];i;i=nxt[i])
	{
		int t=to[i];
		if(t==fa)continue;
		dfs(t,now);//深搜 
		f[now][1]+=min({f[t][0],f[t][1],f[t][2]});//now自己守自己
		//儿子三种情况选最小 
		f[now][2]+=min(f[t][1],f[t][0]);//now不守自己 
		//儿子可以自己守可以让儿子守 
		f[now][0]+=min(f[t][1],f[t][0]);//同上 
		mi=min(mi,f[t][1]-min(f[t][1],f[t][0]));
	}
	f[now][0]+=mi;//如果没人守就让权重最小的儿子守或自己守 
	f[now][1]+=v[now];//自己守要加上自己的权重 
}

for(int i=1;i<=n;i++)
{
	scanf("%d%d%d",&id,&w,&son);
	v[id]=w;
	for(int j=1;j<=son;j++)
	{
		scanf("%d",&so);
		addedge(id,so);
		addedge(so,id);
	}
}
dfs(1,0);//从1开始搜 
printf("%d",min(f[1][0],f[1][1]));//2是父亲守自己,根没有父亲 
//不参与比较 

三色二叉树

给出一串序列0表示无子树,1表示有一个子树并给出子树,2同理
有三个颜色红绿蓝,且不能有同色相连或为同一个根的子树
问给出的一棵树中最多有多少个是绿色,最少多少个
同样是要先建树

void buildtree(int x)
{
	sum++;//记录节点赋值1,2…… 
	if(s[x]=='0') return;
	else if(s[x]=='1') //只有左子树 
	{
		son[x][1]=sum+1;
		buildtree(sum+1);
		return;
	}
	else if(s[x]=='2')//左和右 
	{
		son[x][1]=sum+1;
		buildtree(sum+1);
		son[x][2]=sum+1;
		buildtree(sum+1);
		return;
	}
}

void dfs(int x)
{
	f[x][0]=g[x][0]=1;//绿色是0其他两色是1,2 
	if(son[x][1]&&son[x][2])//是否有两个孩子 
	{
		int l=son[x][1],r=son[x][2];
		dfs(l);
		dfs(r);
		f[x][0]=max(f[l][1]+f[r][2],f[l][2]+f[r][1])+1;
		f[x][1]=max(f[l][0]+f[r][2],f[l][2]+f[r][0]);
		f[x][2]=max(f[l][0]+f[r][1],f[l][1]+f[r][0]);
		g[x][0]=min(g[l][1]+g[r][2],g[l][2]+g[r][1])+1;
		g[x][1]=min(g[l][0]+g[r][2],g[l][2]+g[r][0]);
		g[x][2]=min(g[l][0]+g[r][1],g[l][1]+g[r][0]);
	}
	else if(son[x][1]&&!son[x][2])//只有一个孩子 
	{
		int l=son[x][1];
		dfs(l);
		f[x][0]=max(f[l][1],f[l][2])+1;
		f[x][1]=max(f[l][0],f[l][2]);
		f[x][2]=max(f[l][0],f[l][1]);
		g[x][0]=min(g[l][1],g[l][2])+1;
		g[x][1]=min(g[l][0],g[l][2]);
		g[x][2]=min(g[l][0],g[l][1]);
	}
}//加一是因为用等号连接,虽然绿色置为1了但被覆盖了需要重新加一下 

选课

给定选课个数和每节课的先修课0是没有先修课,给出每种课的学分
求最大学分

//建树略 
for(int i=1;i<=n;i++)
{
	scanf("%d%d",&y,&f[i][1]);
	addedge(y,i);//只加一条边父亲指向儿子 
}
//存学分直接存入dp 
//因为可能有多个节点没有先修课,可以设一个超级原点此时能选修的 
//课要+1把超级原点算上超级原点学分为零 
//从零开始深搜 
//树上背包 
void dfs(int u)
{
	for(int i=h[u];i;i=nxt[i])//遍历每个子树 
	{
		dfs(to[i]);
		for(int j=m;j;j--)//遍历容量为j时的最大值01背包 
		{
			for(int k=0;k<j;k++)//当前子树占用当前树的容量 
			{
				f[u][j]=max(f[u][j],f[u][j-k]+f[to[i]][k]);
			}
		}
	}
}
posted @ 2024-03-10 14:43  晨曦ccx  阅读(52)  评论(0)    收藏  举报
浏览器标题切换
浏览器标题切换end