热门标签 | 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\)





推荐阅读
  • 本文将探讨Java编程语言中对象和类的核心概念,帮助读者更好地理解和应用面向对象编程的思想。通过实际例子和代码演示,我们将揭示如何在Java中定义、创建和使用对象。 ... [详细]
  • Hadoop发行版本选择指南:技术解析与应用实践
    本文详细介绍了Hadoop的不同发行版本及其特点,帮助读者根据实际需求选择最合适的Hadoop版本。内容涵盖Apache Hadoop、Cloudera CDH等主流版本的特性及应用场景。 ... [详细]
  • 采用IKE方式建立IPsec安全隧道
    一、【组网和实验环境】按如上的接口ip先作配置,再作ipsec的相关配置,配置文本见文章最后本文实验采用的交换机是H3C模拟器,下载地址如 ... [详细]
  • Python 内存管理机制详解
    本文深入探讨了Python的内存管理机制,涵盖了垃圾回收、引用计数和内存池机制。通过具体示例和专业解释,帮助读者理解Python如何高效地管理和释放内存资源。 ... [详细]
  • 本文介绍了如何在多线程环境中实现异步任务的事务控制,确保任务执行的一致性和可靠性。通过使用计数器和异常标记字段,系统能够准确判断所有异步线程的执行结果,并根据结果决定是否回滚或提交事务。 ... [详细]
  • 本文详细介绍了如何在不同操作系统和设备上设置和配置网络连接的IP地址,涵盖静态和动态IP地址的设置方法。同时,提供了关于路由器和机顶盒等设备的IP配置指南。 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • 本文详细解析了Java中hashCode()和equals()方法的实现原理及其在哈希表结构中的应用,探讨了两者之间的关系及其实现时需要注意的问题。 ... [详细]
  • 使用Python计算文件的CRC32校验值
    本文记录了一次对路由器固件分析时,如何利用Python计算文件的CRC32校验值。文中提供了完整的代码示例,并详细解释了实现过程。 ... [详细]
  • 丽江客栈选择问题
    本文介绍了一道经典的算法题,题目涉及在丽江河边的n家特色客栈中选择住宿方案。两位游客希望住在色调相同的两家客栈,并在晚上选择一家最低消费不超过p元的咖啡店小聚。我们将详细探讨如何计算满足条件的住宿方案总数。 ... [详细]
  • JSOI2010 蔬菜庆典:树结构中的无限大权值问题
    本文探讨了 JSOI2010 的蔬菜庆典问题,主要关注如何处理非根非叶子节点的无限大权值情况。通过分析根节点及其子树的特性,提出了有效的解决方案,并详细解释了算法的实现过程。 ... [详细]
  • 题目描述:给定一个N*M的网格,初始时网格中有k个芯片,每个芯片的位置已知。玩家可以在每一步操作中将所有芯片沿同一方向移动一格。如果芯片到达边界,则保持不动。目标是通过一系列操作,使每个芯片依次访问指定的目标位置。 ... [详细]
  • 本文介绍了如何利用Python进行批量图片尺寸调整,包括放大和等比例缩放。文中提供了详细的代码示例,并解释了每个步骤的具体实现方法。 ... [详细]
  • 本题要求实现一个函数,用于检查给定的字符串是否为回文。回文是指正向和反向读取都相同的字符串。例如,“XYZYX”和“xyzzyx”都是回文。 ... [详细]
  • Java 实现二维极点算法
    本文介绍了一种使用 Java 编程语言实现的二维极点算法。该算法用于从一组二维坐标中筛选出极点,适用于需要处理几何图形和空间数据的应用场景。文章不仅详细解释了算法的工作原理,还提供了完整的代码示例。 ... [详细]
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社区 版权所有