DP——从入门到放弃 [Did Not Finish]

Part 00 dp 什么时候用?

首先,如果这题数据范围一眼 BFS/DFS/暴力/随机化 可以过,那么还写 dp 干什么
但是,但你决定这题要贪心时,我建议咱还是要看一下它对不对
整一个石子合并这样的就很难受

其次,除了数位 dp 以外正常 dp 数据范围不会特别大 至少要保证 DP 数组不会 MLE

最重要的是,dp 的根本思路是记录以前的信息进行推算

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 —— OI-WiKi

听着有点分治的意思

同时 dp 一般求的是最优解(数位 dp 除外)
很多人都强调,dp 的问题不能有后效性,这个我们通过以后例题来看 作者表示自己不懂

最后,dp 里有两个东西叫状态和转移
状态是指当前的一个状态
例如 "现在作者很蒟蒻" 这就是一个状态
转移则是指当前状态和上一步状态之间的关系
for example
现在作者=(一年前更蒟蒻的作者+一年的学习)
这就是一个转移
转移也可能是 现在作者=(一年前一样蒟蒻的作者+一年的唐)

Part 01 简单 dp

有一部分的 dp 题目没有任何的技巧
在 OI 中属于送分题
接下来我们直接
上!例!题!

T1 ACP1915 数字金字塔
题面友好不多解释
直接 Solution
dp[i][j] 为在 [i,j] 位置的最大收益
难点在于处理金字塔
如图所示
image

图中我们发现 dp[i][j] 实际上是从 dp[i1][j]dp[i1][j1] 转移而来的(图中的粉色点是由蓝色点转移而来的)
So 转移方程也很简单啦
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+mp[i][j])

现在思考边界
发现不用处理设 0 即可
由于数据非负也不需要特判了

Code

#include<bits/stdc++.h>
#define Routine for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)
using namespace std;
const int N=510;
int mp[N][N],dp[N][N],n,ans=-1;
int main(){
	scanf("%d",&n);
	Routine scanf("%d",&mp[i][j]);
	Routine dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+mp[i][j];
	for(int i=1;i<=n;i++)ans=max(ans,dp[n][i]);
	printf("%d\n",ans);
	return 0;
}

 
T2 B3637 最长上升子序列
dp[i] 为以第 i 个数结尾的最长上升子序列长度
只要在前面找一个结尾 value 比他小的最长的最长上升子序列接上去就对了
Code

#include<bits/stdc++.h>
using namespace std;
const int N=5505;
int lable[N],dp[N],n,tmp;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&lable[i]);
	for(int i=1;i<=n;i++){
		tmp=0;
		for(int j=1;j<i;j++)if(lable[i]>lable[j])tmp=max(tmp,dp[j]);
		dp[i]=tmp+1;
	}
	tmp=0;
	for(int i=1;i<=n;i++)tmp=max(tmp,dp[i]);
	printf("%d\n",tmp);
	return 0;
}

 
T2.1 ACP1916 最长不降子序列
题目基本同 T2
AcCoders 硬核题目又双叒叕写错了,就是最长上升
只不过要求输出方案
只要记录转移的来源 DFS 即可(很多题都是如此)
为此还需要开一个虚点来记录末尾的位置
Code

#include<bits/stdc++.h>
using namespace std;
const int N=5505;
int lable[N],dp[N],n,tmp,GoPrevious[N];
void Dfs(int id){
	if(!~id)return;
	Dfs(GoPrevious[id]);
	if(id==n)return;
	printf("%d ",lable[id]);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&lable[i]);
	++n;
	lable[n]=107473813;
	for(int i=1;i<=n;i++){
		tmp=0,GoPrevious[i]=-1;
		for(int j=1;j<i;j++)if(lable[i]>lable[j])if(tmp<=dp[j]/*AcCoders 无 SPJ 要求 tmp 处取等*/)tmp=dp[j],GoPrevious[i]=j;
		dp[i]=tmp+1;
		//printf("dp[%d]=%d prev[%d]=%d\n",i,dp[i],i,GoPrevious[i]);
	}
	printf("max=%d\n",dp[n]-1);
	Dfs(n);
	return 0;
}
/*
7
1 3 2 4 3 5 3
*/

T2.2 NOIP1999 导弹拦截
题面非常清晰
第一问所求就是最长不降子序列
并且还有善良的 SPJ
So 40/200 pts Code...

#include<bits/stdc++.h>
using namespace std;
const int N=1009800;
int lable[N],dp[N],n,tmp;
int main(){
	while(scanf("%d",&lable[++n])!=EOF);
	lable[n]=-1;
	for(int i=1;i<=n;i++){
		tmp=0;
		for(int j=1;j<i;j++)if(lable[i]<=lable[j])tmp=max(tmp,dp[j]);
		dp[i]=tmp+1;
	}
	printf("%d\n0",dp[n]-1);
	return 0;
}

发现此时 O(n2) 复杂度第一问已经拿到了 AC
考虑如何处理第二问
这是一个贪心的思想
只需将每一个系统最后一发炮弹的高度记录下来
判断能否加入以前的系统
若可以则加入合法的高度最低的系统
若不行就新开一个系统
可证这样做后序列满足单调不降
于是找最低合法系统就可以二分
总复杂度 O(n log n)
可以过掉所有数据
经过适当的特判避免 TLE 卡掉第二问分数
能拿 140 pts
AcCoders ACP1917 已经可以了
Code

#include<bits/stdc++.h>
#define mid ((l+r)>>1)
using namespace std;
const int N=1009800;
int lable[N],dp[N],n,ender[N],cnt;
void Solve_First(){
	if(n>20000){
		printf("0");
		return;
	}
	int tmp;
	for(int i=1;i<=n;i++){
		tmp=0;
		for(int j=1;j<i;j++)if(lable[i]<=lable[j])tmp=max(tmp,dp[j]);
		dp[i]=tmp+1;
	}
	printf("%d",dp[n]-1);
}
int Shit(int key){
	int l=1,r=cnt;
	while(l<r){
		if(ender[mid]>=key)r=mid;
		else l=mid+1;
	}
	return l;
}
void Solve_Second(){
	for(int i=1;i<n;i++){
		if(cnt==0||ender[cnt]<lable[i])ender[++cnt]=lable[i];
		else ender[Shit(lable[i])]=lable[i];
	}
	printf("%d",cnt);
}
int main(){
	while(scanf("%d",&lable[++n])!=EOF);
	lable[n]=-1;
	Solve_First();
	puts("");
	Solve_Second();
	return 0;
}

现在思考如何 O(n log n) 处理最长不升子序列
这个方法也有很多,此处我们使用树状数组方法
我们重新分析一下求最长不升子序列的过程
首先枚举以每一个元素为结尾 很明显无法压缩时间复杂度
接下来找到前面 value 比他小的 dp 值最大的位置并接上去
这个东西有点类似于二维偏序
可以用树状数组维护
瞅一眼炮弹高度数据范围 [1,50000]
根本不需要加一或者离散化之类的操作
直接填进去维护最小值即可
蒟蒻作者能不能写出来也很存疑
注意此处要求 value 大于当前元素的
所以我们可以用一个数减去 value 作为下标存进树状数组
Code

#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define lowbit (id&-id)
using namespace std;
const int N=100980,M=150073;
class Tree{
	public:
		void set_size(int _size){size=_size;}
		void modify(int id,int value){while(id<=size)data[id]=max(data[id],value),id+=lowbit;}
		int query(int id){
			int ans=-1;
			while(id>=1)ans=max(ans,data[id]),id-=lowbit;
			return ans;
		}
	private:
		int data[M+10],size;
};
Tree tree;
int lable[N],dp[N],n,ender[N],cnt;
void Solve_First(){
	int ans=-1;
	for(int i=1;i<=n;i++){
		dp[i]=tree.query(50089-lable[i])+1;
		ans=max(ans,dp[i]);
		tree.modify(50089-lable[i],dp[i]);
	}
	printf("%d",ans);
}
int Shit(int key){
	int l=1,r=cnt;
	while(l<r){
		if(ender[mid]>=key)r=mid;
		else l=mid+1;
	}
	return l;
}
void Solve_Second(){
	for(int i=1;i<=n;i++){
		if(cnt==0||ender[cnt]<lable[i])ender[++cnt]=lable[i];
		else ender[Shit(lable[i])]=lable[i];
	}
	printf("%d",cnt);
}
int main(){
	tree.set_size(M);
	while(scanf("%d",&lable[++n])!=EOF);
	--n;
	Solve_First();
	puts("");
	Solve_Second();
	return 0;
}

成功 AC luogu 题目
AcCoders 双倍经验

T3 ACP1918 城市交通路网
本题题面抽象
简单说就是给一个邻接矩阵
跑出从一号点到 n 号点的最短路
数据范围没给 AcCoders 不愧是他
推测应该 1<=n<=2000
也MarkDown的不知道有没有负权
要求输出距离和路径
作者也不知道这和 DP 有啥关系
写个 Dijkstra 水过吧
思考如何输出路径
考虑记录每个点的前驱
在元素到达堆顶时记录
Code

#include<bits/stdc++.h>
using namespace std;
const int N=100073;
struct line{int to,len;};
struct Node{
	int id,key,Previous;
	bool operator <(const Node &rhs)const{
		 return key>rhs.key;
	}
};
priority_queue<Node> q;
vector<line> lable[N];
int Distance[N],Previous[N],n;
bool vis[N];
void Dijkstra(int start){
	memset(Distance,0x3f3f3f,sizeof(Distance));
	Distance[start]=0;
	q.push({start,0,-1});
	while(!q.empty()){
		Node top=q.top();
		q.pop();
		if(vis[top.id])continue;
		vis[top.id]=true;
		Previous[top.id]=top.Previous;
		for(auto it:lable[top.id]){
			if(Distance[it.to]>=Distance[top.id]+it.len){
				Distance[it.to]=Distance[top.id]+it.len;
				q.push({it.to,Distance[it.to],top.id});
			}
		}
	}
}
void Dfs(int id){
	if(~id){
		Dfs(Previous[id]);
		printf("%d ",id);
	}
}
int main(){
	int tmp;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
		scanf("%d",&tmp);
		if(tmp)lable[i].push_back({j,tmp});
	}
	Dijkstra(1);
	printf("minlong=%d\n",Distance[n]);
	Dfs(n);
	puts("");
	return 0;
}

T4 ACP1919 挖地雷
这题题面还是比较清晰的
如果不细看这题是一道
Dijkstra+++ 的题
直到你看到了

保证 x<y

于是这个就没有后效性了
可以用 DP 来做
设 dp[i] 为到了第 i 个地窖的最大收益
只需要在自己的前驱中找到受益最大的加上当前地窖收益即可
注意最终答案不一定是 dp[n]
需要取 DPmax
至于输出路径 DFS 即可
Code

#include<bits/stdc++.h>
using namespace std;
const int N=1009;
int n,lable[N],dp[N],Front[N];
vector<int> Previous[N];
void Dfs(int id){
	if(id==-1){
		puts("Holy Shit!");
		return;
	}
	if(Front[id]==-1){
		printf("%d",id);
		return;
	}else{
		Dfs(Front[id]);
		printf("-%d",id);
	}
}
int main(){
	int tmp1,tmp2,tmp;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&lable[i]);
	while(true){
		scanf("%d%d",&tmp1,&tmp2);
		if(tmp1==0||tmp2==0||tmp1>tmp2)break;
		Previous[tmp2].push_back(tmp1);
	}
	for(int i=1;i<=n;i++){
		tmp=0,Front[i]=-1;
		for(auto it:Previous[i])if(dp[it]>tmp)tmp=dp[it],Front[i]=it;
		dp[i]=tmp+lable[i];
	}
	tmp=-1,tmp1=-1;
	for(int i=1;i<=n;i++)if(tmp<dp[i])tmp=dp[i],tmp1=i;
	Dfs(tmp1);
	printf("\n%d",tmp);
	return 0;
}
posted on   2025ing  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee
点击右上角即可分享
微信分享提示