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 数字金字塔
题面友好不多解释
直接
设
难点在于处理金字塔
如图所示
图中我们发现
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+mp[i][j])
现在思考边界
发现不用处理设
由于数据非负也不需要特判了
#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 最长上升子序列
设
只要在前面找一个结尾
#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
#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;
}
发现此时
考虑如何处理第二问
这是一个贪心的思想
只需将每一个系统最后一发炮弹的高度记录下来
判断能否加入以前的系统
若可以则加入合法的高度最低的系统
若不行就新开一个系统
可证这样做后序列满足单调不降
于是找最低合法系统就可以二分
总复杂度
可以过掉所有数据
经过适当的特判避免
能拿
过 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;
}
现在思考如何
这个方法也有很多,此处我们使用树状数组方法
我们重新分析一下求最长不升子序列的过程
首先枚举以每一个元素为结尾 很明显无法压缩时间复杂度
接下来找到前面
这个东西有点类似于二维偏序
可以用树状数组维护
瞅一眼炮弹高度数据范围
根本不需要加一或者离散化之类的操作
直接填进去维护最小值即可
蒟蒻作者能不能写出来也很存疑
注意此处要求
所以我们可以用一个数减去
#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;
}
成功
AcCoders 双倍经验
T3 ACP1918 城市交通路网
本题题面抽象
简单说就是给一个邻接矩阵
跑出从一号点到
数据范围没给 AcCoders 不愧是他
推测应该
也MarkDown的不知道有没有负权
要求输出距离和路径
作者也不知道这和
写个 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 挖地雷
这题题面还是比较清晰的
如果不细看这题是一道
直到你看到了
保证
于是这个就没有后效性了
可以用
设 dp[i] 为到了第 i 个地窖的最大收益
只需要在自己的前驱中找到受益最大的加上当前地窖收益即可
注意最终答案不一定是
需要取
至于输出路径
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;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee