热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入探讨:DP技术的消亡与未来趋势

本文深入探讨了动态规划(DP)技术的发展历程及其未来趋势。文章首先详细介绍了DP的基本步骤,包括定义状态、寻找状态转移方程和确定边界条件。通过分析这些核心概念,文章揭示了DP在解决复杂问题中的重要性和局限性,并展望了其未来的发展方向。此外,文章还讨论了当前研究中的一些新兴技术和方法,这些技术有望进一步提升DP的效率和适用范围。

一般的DP的步骤



  1. 定义状态:找通解

    1. 关注题目需要的东西

    2. 状态确定,输出就确定

    3. 将状态的准确描述记录下来



  2. 寻找状态转移方程:描述一个子问题如何用更小的子问题得到

  3. 确定边界条件:最小的子问题或不满足状态转移方程的状态


一些小小的DP

例1. 摆花

先看一眼题面,容易设计状态。

dp[i][j] 表示前 \(i\) 种花一共摆了 \(j\) 盆时的种类数。

然后让每一位加上前面可以达到的位的答案。

答案即为 dp[n][m]

代码实现:

int main()
{
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;i++)
scanf("%d",&a[i]);
dp[0][0]=1;
for(register int i=1;i<=n;i++)
for(register int j=0;j<=m;j++)
for(register int k=0;k<=a[i];k++)
if(j>=k)dp[i][j]=(dp[i][j]+dp[i-1][j-k])%MOD;//需要保证摆的花不超过j盆且不超过限制a[i]
printf("%d",dp[n][m]);
return 0;
}

例2. 木棍加工

一眼可以看出是求下降子序列的个数。

根据某个著名的定理,下降子序列的个数等于最长上升子序列的长度。

根据一维排序再求最长上升子序列长度即可。

代码实现:

struct node
{
int l,r;
}e[MAXN];
int n;
inline bool cmp(node x,node y)
{
if(x.l==y.l)return x.r>y.r;
return x.l>y.l;
}
int dp[MAXN];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d%d",&e[i].l,&e[i].r);
sort(e+1,e+1+n,cmp);
for(register int i=1;i<=n;i++)
{
for(register int j=1;j if(e[j].r }
int maxn=-0x7f7f7f7f;
for(register int i=1;i<=n;i++)
maxn=max(maxn,dp[i]);
printf("%d",maxn+1);
return 0;
}

例3. 书本整理

不整齐度和剩下的书有关,所以状态和剩下的书有关。

dp[i][j] 表示前 \(i\) 本书中留下了 \(j\) 本且第 \(i\) 本一定选的最小不整齐度。

寻找每一个可以与当前遍历到的 \(i\) 匹配的 \(p\) 即可。

代码实现:

int main()
{
scanf("%d%d",&n,&k);
for(register int i=1;i<=n;i++)
scanf("%d%d",&e[i].l,&e[i].r);
sort(e+1,e+1+n,cmp);
for(register int i=1;i<=n;i++)
for(register int j=1;j<=n-k;j++)
dp[i][j]=0x7f7f7f7f;
for(register int i=1;i<=n;i++)
dp[i][0]=dp[i][1]=0;
for(register int i=1;i<=n;i++)
for(register int j=1;j<=min(i,n-k);j++)
for(register int p=j-1;p<=i-1;p++)
dp[i][j]=min(dp[i][j],dp[p][j-1]+abs(e[i].r-e[p].r));
int minn=0x7f7f7f7f;
for(register int i=n-k;i<=n;i++)
minn=min(minn,dp[i][n-k]);
printf("%d",minn);
return 0;
}

例4. Modulo Sum

直接搞DP是 \(O(nm)\) 的,肯定会炸。

考虑一个小优化:当 \(n\ge m\) 时必定有解。

我们假设 \(dp_{i,j}\) 表示考虑在前 \(i\) 个数中选数,是否可能使得它们的和除以 \(m\) 的余数为 \(j\) ,初始状态 \(dp_{i,a_{i}}=1\),枚举每个数和余数进行转移即可。

代码实现:

#include
using namespace std;
const int MAXN=1e3+5;
int n,m;
bool dp[MAXN][MAXN];
int a[MAXN];
int main()
{
scanf("%d%d",&n,&m);
if(n>m)
{
puts("YES");
return 0;
}
for(register int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
a[i]%=m;
dp[i][a[i]]=1;
if(!a[i])
{
puts("YES");
return 0;
}
}
for(register int i=1;i<=n;i++)
{
for(register int j=0;j {
dp[i][j]|=dp[i-1][j];
dp[i][(j+a[i])%m]|=dp[i-1][j];
}
if(dp[i][0])
{
puts("YES");
return 0;
}
}
puts("NO");
return 0;
}

现在有一点图的变化。

例1. 挖地雷

有很多方法,比如暴搜

可能第一印象是拓扑+DP

手玩可以发现,一定是从编号小的地窖跑到编号较大的地窖。

所以二维循环即可。

代码实现:


区间DP

一般是求一个区间内的最大值,最小值,方案数。

判别:



  • 从不同位置开始递推得到的结果可能不一样。



  • 合并类或拆分类。



区间DP一般有三个循环:



  • 第一个循环一般是枚举阶段(子问题)



  • 第二个循环枚举所有的状态(情形)



  • 第三个循环枚举决策点(从哪里转移)



例1. 石子合并(加强版)

区间DP/GarsiaWachs算法板子题

GarsiaWachs算法是专门解决石子合并问题,好像是线性复杂度。

算法流程大致如下:



  1. 寻找最小的满足 \(a_{k-1}\leqslant a_{k+1}\)\(k\) ,将 \(a_{k}\)\(a_{k-1}\) 合并。



  2. \(k\) 向前寻找第一个满足 \(a_j\gt a_k+a_{k-1}\)\(j\) ,将 \(a_k+a_{k-1}\) 插入在 \(a_j\) 后面。



  3. 当只剩下一个数时,那个数就是答案。



区间DP:

#include
using namespace std;
const int MAXN=305;
int n,val[MAXN],sum[MAXN];
int dp[MAXN][MAXN];
int main()
{
scanf("%d",&n);
memset(dp,0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
sum[i]=sum[i-1]+val[i];
dp[i][i]=0;
}
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
for(register int k=i;k dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
printf("%d",dp[1][n]);
return 0;
}

GarsiaWachs:

#include
#define int long long
using namespace std;
int ans,n;
vectorv;
inline int merge()
{
int k=v.size()-2;
for(register int i=0;i if(v[i]<=v[i+2])
{
k=i;
break;
}
int now=v[k]+v[k+1];
v.erase(v.begin()+k);
v.erase(v.begin()+k);
int wh=-1;
for(register int i=k-1;i>=0;i--)
if(v[i]>now)
{
wh=i;
break;
}
v.insert(v.begin()+wh+1,now);
return now;
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i<=n;i++)
{
int op;
scanf("%lld",&op);
v.push_back(op);
}
for(register int i=1;i ans+=merge();
printf("%lld",ans);
return 0;
}

例2. 248 G

还是区间DP板子。

只要两边相等,就可以将两边合并。

代码实现:

#include
using namespace std;
const int MAXN=305;
int n,val[MAXN];
int dp[MAXN][MAXN];
int main()
{
scanf("%d",&n);
memset(dp,-0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
dp[i][i]=val[i];
}
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
for(register int k=i;k if(dp[i][k]==dp[k+1][j])dp[i][j]=max(dp[i][j],dp[i][k]+1);
}
int maxn=0;
for(register int i=1;i<=n;i++)
for(register int j=i;j<=n;j++)
maxn=max(maxn,dp[i][j]);
printf("%d",maxn);
return 0;
}

例3. Cheapest Palindrome G

对于一个区间,强制它要是一个回文串。

所以对于每一个区间 \([i,j]\) ,有以下转移:

\(dp_{i,j}=\min\begin{cases}dp_{i+1,j}+\min(ins_i,del_i)\\dp_{i,j-1}+\min(ins_j,del_j)\\\text{if }a_i=a_j~~~~~~~~~~dp_{i+1,j-1}\end{cases}\)

代码实现:

#include
using namespace std;
const int MAXN=2005;
int n,m;
string a;
int dp[MAXN][MAXN];
mapins,del;
signed main()
{
scanf("%d%d",&n,&m);
cin>>a;
a=' '+a;
memset(dp,0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
char op;
int in,de;
cin>>op>>in>>de;
ins[op]=in;
del[op]=de;
}
for(register int i=1;i<=m;i++)
dp[i][i]=0;
for(register int len=2;len<=m;len++)
for(register int i=1;i+len-1<=m;i++)
{
int j=i+len-1;
if(a[i]==a[j])
{
if(len==2)dp[i][j]=0;
else dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
}
dp[i][j]=min(dp[i][j],dp[i][j-1]+ins[a[j]]);
dp[i][j]=min(dp[i][j],dp[i][j-1]+del[a[j]]);
dp[i][j]=min(dp[i][j],dp[i+1][j]+ins[a[i]]);
dp[i][j]=min(dp[i][j],dp[i+1][j]+del[a[i]]);
}
printf("%d",dp[1][m]);
return 0;
}

例4. 合唱队

首先套路设 \(dp_{i,j}\) 为区间 \([i,j]\) 的方案数。

然后发现状态设计有问题。

所以再加一维表示最后一个是从哪边进来的。

有一个小坑点:只有一个人时从左边右边进来都一样,只要初始化一边即可。

代码实现:

#include
using namespace std;
const int MAXN=2005;
const int MOD=19650827;
int n;
int a[MAXN];
int dp[MAXN][MAXN][2];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(register int i=1;i<=n;i++)
dp[i][i][0]=1;
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
if(a[i]a[i])dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][0])%MOD;
if(a[j]>a[j-1])dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][1])%MOD;
}
printf("%d",(dp[1][n][0]+dp[1][n][1])%MOD);
return 0;
}

例5. 关路灯

有了上一题的经验,一上来就可以设 \(dp_{i,j,0/1}\) 表示在区间 \([i,j]\) 内最后一个是 \(i/j\) 的最小代价。

转移也十分套路,按照思路模拟即可。

需要注意的是初始化。

因为给定了初始位置为 \(c\)

所以 \(dp_{i,i}=|a_c-a_i|\times (sum_n-w_c)\)

代码实现:

#include
using namespace std;
const int MAXN=55;
int n,c;
int a[MAXN],w[MAXN];
int sum[MAXN];
int dp[MAXN][MAXN][2];
int main()
{
scanf("%d%d",&n,&c);
for(register int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&w[i]);
sum[i]=sum[i-1]+w[i];
}
for(register int i=1;i<=n;i++)
dp[i][i][0]=dp[i][i][1]=abs(a[i]-a[c])*(sum[n]-w[c]);
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
dp[i][j][0]=min(dp[i+1][j][0]+(a[i+1]-a[i])*(sum[n]+sum[i]-sum[j]),dp[i+1][j][1]+(a[j]-a[i])*(sum[n]+sum[i]-sum[j]));
dp[i][j][1]=min(dp[i][j-1][0]+(a[j]-a[i])*(sum[n]+sum[i-1]-sum[j-1]),dp[i][j-1][1]+(a[j]-a[j-1])*(sum[n]+sum[i-1]-sum[j-1]));
}
printf("%d",min(dp[1][n][0],dp[1][n][1]));
return 0;
}

换根DP

对于一类树形DP,若节点不确定,且答案会随着根节点的不同而变换,这种树形DP可以称之为换根DP。

例一. STA-Station

步骤1. 定义 \(dp_i\) 表示以 \(i\) 为根节点的子树的最大深度和,然后任意指定节点跑一遍树形DP。

步骤2. 定义 \(f_i\) 表示以 \(i\) 为全局根节点时的最大深度和,然后以 \(rt\) 再跑一遍树形DP。

步骤3. 在 \(f_1\)\(f_n\) 中取最大值即为答案。

代码实现:

#include
#define int long long
using namespace std;
const int MAXN=2e6+5;
struct node
{
int to,nxt;
}e[MAXN];
int head[MAXN],cnt;
inline void add(int x,int y)
{
e[++cnt].to=y;
e[cnt].nxt=head[x];
head[x]=cnt;
}
int n;
int f[MAXN],dep[MAXN],siz[MAXN];
inline void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;
siz[x]=1;
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(y==fa)continue;
dfs1(y,x);
siz[x]+=siz[y];
}
}
inline void dfs2(int x,int fa)
{
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(y==fa)continue;
f[y]=f[x]-siz[y]+(n-siz[y]);
dfs2(y,x);
}
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i {
int x,y;
scanf("%lld%lld",&x,&y);
add(x,y);
add(y,x);
}
dfs1(1,0);
for(register int i=1;i<=n;i++)
f[1]+=dep[i];
dfs2(1,0);
int maxn=-0x7f7f7f7f,id;
for(register int i=1;i<=n;i++)
if(f[i]>maxn)
{
maxn=f[i];
id=i;
}
printf("%lld",id);
return 0;
}

例二. Great Cow Gathering G

差不多,也是板子。

代码实现:

#include
#define int long long
using namespace std;
const int MAXN=4e5+5;
struct node
{
int to,nxt,len;
}e[MAXN];
int head[MAXN],cnt;
inline void add(int x,int y,int z)
{
e[++cnt].to=y;
e[cnt].len=z;
e[cnt].nxt=head[x];
head[x]=cnt;
}
int n,sum;
int val[MAXN];
int dp[MAXN],f[MAXN],siz[MAXN];
inline void dfs1(int x,int fa)
{
siz[x]=val[x];
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(y==fa)continue;
dfs1(y,x,i);
siz[x]+=siz[y];
dp[x]=dp[x]+dp[y]+siz[y]*z;
}
}
inline void dfs2(int x,int fa)
{
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(y==fa)continue;
f[y]=f[x]-siz[y]*z+(sum-siz[y])*z;
dfs2(y,x);
}
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i<=n;i++)
{
scanf("%lld",&val[i]);
sum+=val[i];
}
for(register int i=1;i {
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
dfs1(1,0,0);
f[1]=dp[1];
// cout< dfs2(1,0);
int minn=0x7f7f7f7f7f7f;
for(register int i=1;i<=n;i++)
minn=min(minn,f[i]);
printf("%lld",minn);
return 0;
}

状压DP

定义:当状态的维度很多,而每一个维度的取值是bool值时,则可以用二进制数值去表示一个状态,这种DP被称为状压DP

基本的位运算:

按位与( \(\&\) ):两个整数在二进制下逐位比较,同一位有 \(2\)\(1\) ,则结果为 \(1\) ,否则为 \(0\)

按位或( \(|\) ):两个整数在二进制下逐位比较,同一位有 \(1\) ,则结果为 \(1\) ,否则为 \(0\)

按位异或( \(^\) ): 两个整数在二进制下逐位比较,同一位不同则为 \(1\) ,否则为 \(0\)

按位取反( ~ ):字面意思。

常见位操作意义:



  • 一个二进制数位 &1 得到本身



  • 一个二进制数位 ^1 取反



  • 一个二进制数位 &0 则赋值为 \(0\)



  • 一个二进制数位 |1 则赋值为 \(1\)



  • (n>>k)&1 取出二进制下 \(n\) 的第 \(k\) 位(从右往左)



  • n&((1< 取出二进制下 \(n\) 的右 \(k\)



  • n^(1< 将二进制下的第 \(k\) 位取反



  • n|(1< 将二进制下的第 \(k\) 位赋值 \(1\)



  • n&(~(1< 将二进制下 \(n\) 的第 \(k\) 位赋值 \(0\)





推荐阅读
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 本文将详细介绍Python中*args和**kwargs的用法,包括它们的基本概念、应用场景以及注意事项。 ... [详细]
  • 单片微机原理P3:80C51外部拓展系统
      外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。0.IO接口电路概念与存 ... [详细]
  • 使用多项式拟合分析淘宝双11销售趋势
    根据天猫官方数据,2019年双11成交额达到2684亿元,再次刷新历史记录。本文通过多项式拟合方法,分析并预测未来几年的销售趋势。 ... [详细]
  • 多线程基础概览
    本文探讨了多线程的起源及其在现代编程中的重要性。线程的引入是为了增强进程的稳定性,确保一个进程的崩溃不会影响其他进程。而进程的存在则是为了保障操作系统的稳定运行,防止单一应用程序的错误导致整个系统的崩溃。线程作为进程的逻辑单元,多个线程共享同一CPU,需要合理调度以避免资源竞争。 ... [详细]
  • MySQL 5.7 学习指南:SQLyog 中的主键、列属性和数据类型
    本文介绍了 MySQL 5.7 中主键(Primary Key)和自增(Auto-Increment)的概念,以及如何在 SQLyog 中设置这些属性。同时,还探讨了数据类型的分类和选择,以及列属性的设置方法。 ... [详细]
  • 本文对比了杜甫《喜晴》的两种英文翻译版本:a. Pleased with Sunny Weather 和 b. Rejoicing in Clearing Weather。a 版由 alexcwlin 翻译并经 Adam Lam 编辑,b 版则由哈佛大学的宇文所安教授 (Prof. Stephen Owen) 翻译。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 思科IOS XE与ISE集成实现TACACS认证配置
    本文详细介绍了如何在思科IOS XE设备上配置TACACS认证,并通过ISE(Identity Services Engine)进行用户管理和授权。配置包括网络拓扑、设备设置和ISE端的具体步骤。 ... [详细]
  • 本文提供了一个C++程序,用于读取一系列整数并统计其中正整数和负整数的个数。当输入为0时,程序结束。 ... [详细]
  • 最详尽的4K技术科普
    什么是4K?4K是一个分辨率的范畴,即40962160的像素分辨率,一般用于专业设备居多,目前家庭用的设备,如 ... [详细]
  • importpymysql#一、直接连接mysql数据库'''coonpymysql.connect(host'192.168.*.*',u ... [详细]
  • 本文介绍了一个使用C语言实现的程序,用于将给定的时间差转换为标准的时分秒格式。通过输入样例和输出样例,详细解释了核心算法和四舍五入的处理方法。 ... [详细]
  • 本文详细解析了 Codeforces Round #752 (Div. 2) 的 A、B、C、D 四道题目,包括问题描述、解题思路和代码实现。 ... [详细]
author-avatar
一飞冲天的快乐_549
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有