差值 dp 入门

引入

有一类问题:两个人交替选 n n n 个数 a [ 1 … n ] a[1 \dots n] a[1n],要使得每个人分得的数大小之和相等(或差值尽可能小),同时尽可能保证分得的总金额尽可能大。

这类问题的解法之一是 dp。

有一个通用状态:设 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k] 表示前 i i i 个数,先手得到的数值之和为 j j j,后手得到的数值之和为 k k k 时两人一共得到的最大答案。

我们可以优化状态:设 f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个数,两人分到的数差值为 j j j 时先手能得到的最大钱数。因为我们只关注相对大小,而不关注具体数值。

注:由于状态中存在“差值”概念,所以这类问题被称为差值 dp,属于 01 背包的变种。这类问题的一般暗示会有:谁追上谁,谁减去谁,谁加上谁,谁和谁相等/接近……

如果要保证拿到的数大小之和相等,答案即为 f [ n ] [ 0 ] f[n][0] f[n][0]

考虑转移:

  1. 不用第 i i i 个数: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i1][j]
  2. 把第 i i i 个数给先手: f [ i ] [ j ] = f [ i − 1 ] [ j − a [ i ] ] + a [ i ] f[i][j]=f[i-1][j-a[i]]+a[i] f[i][j]=f[i1][ja[i]]+a[i]
  3. 把第 i i i 个数给后手: f [ i ] [ j ] = f [ i − 1 ] [ j + a [ i ] ] f[i][j]=f[i-1][j+a[i]] f[i][j]=f[i1][j+a[i]]

还有一个问题, j − a [ i ] j-a[i] ja[i] 有可能为负,而数组下标显然不能是负数,有两种解决方法:

  1. 平移法。假设值域 w = ∑ a [ i ] w=\sum a[i] w=a[i],考虑把第二维下标所有的数都加上 w w w,这样负数存储时就变成了正数。相当于把值域往正方向平移了 w w w 个单位长度。注意此时代表 0 0 0 的数是 w w w,所以最终答案应该是 f [ n ] [ w ] f[n][w] f[n][w]

  2. 绝对值法。当最终答案要求两个人取的数之和相等时,我们无需知道大小关系(因为最后一定都相等),所以考虑在减差值时加上一个绝对值来转正: f [ i ] [ j ] = f [ i − 1 ] [ a b s ( j − a [ i ] ) ] + a [ i ] f[i][j]=f[i-1][abs(j-a[i])]+a[i] f[i][j]=f[i1][abs(ja[i])]+a[i]

以上两种方法的使用视情况而定。时间复杂度为 O ( w × n ) O(w \times n) O(w×n)(这也是 01 背包问题的普遍复杂度)。

注:有时改变 dp 状态的意义会对做题更有帮助,请大家学会随机应变。但核心思想是不变的。有一些常见的套路在下面例题中给大家介绍。

例题

例1.

这就是我们上面问题的模板。代码中顺便展示记忆化搜索+剪枝的解法:

/*
1.差值dp
设dp[i][j]表示选了前i个物品,两堆积木差值为j时第一堆积木的高度(这个状态是精髓!)
那么显然有以下三种转移:
不选i,      dp[i][j]<-dp[i-1][j]
选i放第一堆,dp[i][j]<-dp[i-1][j-a[i]]+a[i]
选i放第二堆,dp[i][j]<-dp[i-1][j+a[i]]      // 注意这里不要加上a[i],因为状态存的是第一堆积木的高度还有一个问题,因为存储的是差值,所以第二维可能有负数。
考虑将第二维向上平移500000个,这样0~499999的是负数,500001~1000000的是正数,500000表示0又发现第i行的状态转移只和第i-1行有关,可以滚动数组优化空间------------------------------------------------------------2.爆搜优化:剪枝+记忆化
不加优化爆搜就是01选择类的dfs,判断每个物品是选给第一堆还是第二堆,时间复杂度O(2^n)
考虑先剪枝一下:
1.可行性剪枝。如果当前第一堆加上剩余所有都达不到第二堆高度,显然无解。第二堆同理
2.最优性剪枝。记录当前的最大答案ans,如果第一堆加上剩余所有方块都无法达到ans,显然这个状态不优。如果两堆和剩下所有加起来无法达到2*ans,显然也不优。第二堆同理
3.搜索顺序剪枝。发现答案具有不依赖顺序的特点(按照不同的顺序叠都行,只要最后高度不一样就行)所以考虑对所有积木降序排序。这样DFS时会优先选择较大的数。较大的数更容易接近目标值,从而更快地触发剪枝条件,减少无效搜索。
然后发现dfs会重复搜到许多一样的状态,所以考虑用一个记忆化数组存储当前状态的答案,这样就可以极大优化时间复杂度
由于这道题值域过大,考虑用map来实现记忆化------------------------------------------------------------发现不行的情况即为最大高度为0的情况,输出-1
*/#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxh = 1e6 + 7, zero = 5e5;int n, a[60];// dp解法
void dp(){vector <int> f(maxh,-1e9); // f表示前一行(i-1),g表示当前行(i)vector <int> g(maxh,-1e9); // 初始化为极小值,避免出错f[zero] = 0; // 给一个初状态f[0]for(int i = 1; i <= n; i ++){for(int j = 0; j <= 1e6; j ++){ // 只要遍历到所有元素的和即可g[j] = max(g[j], f[j]);                   // f[i][j]=max(f[i][j],f[i-1][j])if(j >= a[i]){g[j] = max(g[j], f[j - a[i]] + a[i]); // f[i][j]=max(f[i][j],f[i-1][j-a[i]]+a[i])}if(j + a[i] <= 1e6){g[j] = max(g[j], f[j + a[i]]);        // f[i][j]=max(f[i][j],f[i-1][j+a[i]])}}f = g; // 把值滚动给下一行fill(g.begin(),g.end(),-1e9); // 重置g数组的值}cout << (f[zero] == 0? -1 : f[zero]) << '\n'; // 此时因为g的值已经没了,所以要输出f
}// -------------------------------------------------------------------// 搜索解法
int ans = -1e9;
int s[60]; // s是前缀和数组
map< tuple<int,int,int> , int > vis; // 考虑用map存状态int dfs(int now, int h1, int h2){// 终止条件if(now == n + 1){if(h1 == h2){ ans = max(ans, h1); return h1; }else return -1;}// 记忆化tuple <int,int,int> t = {now, h1, h2};if(vis.count(t)){return vis[t];}// 剪枝if(h1+h2 + s[n]-s[now-1] <= 2*ans)	return -1;if(h1 + s[n]-s[now-1] <= ans)		return -1;if(h2 + s[n]-s[now-1] <= ans)		return -1;if(h1 + s[n]-s[now-1] < h2)			return -1;if(h2 + s[n]-s[now-1] < h1)			return -1;// 返回即为三种情况取maxreturn vis[t] = max({ 0/*避免都是-1,和0取一下max*/,dfs(now+1,h1+a[now],h2), dfs(now+1,h1,h2+a[now]), dfs(now+1,h1,h2) });
}void search(){sort(a + 1, a + n + 1, greater<>()); // 降序排序,可以加快搜索(这样不合法的更容易被剪枝)for(int i = 1; i <= n; i ++){s[i] = s[i - 1] + a[i];}int sum = dfs(1, 0, 0);cout << (sum == 0? -1 : sum) << '\n';
}// -------------------------------------------------------------------signed main()
{ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);cin >> n;for(int i = 1; i <= n; i ++){cin >> a[i];}
//	dp();search();return 0;
}

例2.Kas

这类题目还有另一种设状态的方法:设 f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个数,两人分到的数差值为 j j j 时两人一共能得到的最大钱数。

转移:

  1. 不用第 i i i 个数: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i1][j]
  2. 把第 i i i 个数给先手: f [ i ] [ j ] = f [ i − 1 ] [ j − a [ i ] ] + a [ i ] f[i][j]=f[i-1][j-a[i]]+a[i] f[i][j]=f[i1][ja[i]]+a[i]
  3. 把第 i i i 个数给后手: f [ i ] [ j ] = f [ i − 1 ] [ j + a [ i ] ] + a [ i ] f[i][j]=f[i-1][j+a[i]]\textcolor{red}{+a[i]} f[i][j]=f[i1][j+a[i]]+a[i]。(这里的转移有改动)

注:这道题用之前介绍的状态设计方法也可以。这里是为了演示。

得到答案之后,考虑“赌场”的操作。
两人平均分到 x = ⌊ f [ n ] [ 0 ] / 2 ⌋ x=\lfloor f[n][0]/2 \rfloor x=f[n][0]/2 的钱,剩下 ∑ a [ i ] − x \sum a[i]-x a[i]x 的钱去赌场,双倍返现,所以两人都分得 ∑ a [ i ] − x \sum a[i] - x a[i]x 这些钱。

不用担心除不尽,因为题目要求两人拿到的钱数相同,所以 f [ n ] [ 0 ] f[n][0] f[n][0] 一定是一个偶数。

注意这道题还要用滚动数组优化。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1005, maxc = 1e5 + 7;int n, a[maxn], f[2][2 * maxc]; // 开二倍,避免越界
int sum = 0;void solve()
{cin >> n;for(int i = 1; i <= n; i ++){cin >> a[i]; sum += a[i];}memset(f, -0x3f, sizeof f); // 初始化为极小值f[0][0] = 0;for(int i = 1; i <= n; i ++){for(int j = 0; j <= sum; j ++){int x = !(i & 1), y = i & 1;f[y][j] = max({f[x][j], f[x][j + a[i]] + a[i], f[x][abs(j - a[i])] + a[i]});}}cout << sum - f[n & 1][0] / 2 << '\n';
}signed main()
{ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);solve();return 0;
}

例3.小a和uim之大逃离

这道题一看就是差值 dp,不过由线性 dp 被改到了网格上。

先考虑差值 dp 的经典状态:设 f [ i ] [ j ] [ d ] f[i][j][d] f[i][j][d] 表示走到 ( i , j ) (i,j) (i,j),当前两人取的数差值为 d d d 时的方案数。

注意到这道题还有一个“最后一步必须由 uim 吸收”的限制,所以考虑再加一维表示取的人:

f [ i ] [ j ] [ d ] [ 0 / 1 ] f[i][j][d][0/1] f[i][j][d][0/1] 表示走到 ( i , j ) (i,j) (i,j),当前两人取数差值为 d d d,最后一次是 小a/uim 取的方案数。

则转移显然:

  1. uim 取: f [ i ] [ j ] [ d ] [ 1 ] + = f [ i − 1 ] [ j ] [ d − a [ i ] [ j ] ] [ 0 ] f[i][j][d][1]+=f[i-1][j][d-a[i][j]][0] f[i][j][d][1]+=f[i1][j][da[i][j]][0] f [ i ] [ j ] [ d ] [ 1 ] + = f [ i ] [ j − 1 ] [ d − a [ i ] [ j ] ] [ 0 ] f[i][j][d][1]+=f[i][j-1][d-a[i][j]][0] f[i][j][d][1]+=f[i][j1][da[i][j]][0]
  2. 小 a 取: f [ i ] [ j ] [ d ] [ 0 ] + = f [ i − 1 ] [ j ] [ d + a [ i ] [ j ] ] [ 1 ] f[i][j][d][0]+=f[i-1][j][d+a[i][j]][1] f[i][j][d][0]+=f[i1][j][d+a[i][j]][1] f [ i ] [ j ] [ d ] [ 0 ] + = f [ i ] [ j − 1 ] [ d + a [ i ] [ j ] ] [ 1 ] f[i][j][d][0]+=f[i][j-1][d+a[i][j]][1] f[i][j][d][0]+=f[i][j1][d+a[i][j]][1]

初状态: f [ i ] [ j ] [ a [ i ] [ j ] ] [ 0 ] = 1 f[i][j][a[i][j]][0]=1 f[i][j][a[i][j]][0]=1
答案: ∑ f [ i ] [ j ] [ 0 ] [ 1 ] \sum f[i][j][0][1] f[i][j][0][1]

注意还要取模。不光整体答案要对 1 0 9 + 7 10^9 + 7 109+7 取模,因为魔瓶只有 k k k 的容量,所以差值的那一维也要对 k + 1 k +1 k+1 取模(不是 k k k!!!)。

补充:为什么取模的一般都是计数题,因为取模会让一个很大的数突然变得很小,从而无法统计答案。
例如对 1 0 9 10^9 109 取模,会导致 1 0 9 10^9 109 是一个特别小的 0 0 0,而 1 0 9 − 1 10^9-1 1091 却很大,大小关系就乱了。

这道题还有一些卡空间,不能全都开 long long,只在统计答案时开 long long。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;int n, m, k;
int a[801][801];
int f[801][801][20][2];void solve()
{cin >> n >> m >> k;k = k + 1; // 直接赋值为 k+1,好看一点for(int i = 1; i <= n; i ++){for(int j = 1; j <= m; j ++){cin >> a[i][j];f[i][j][a[i][j] % k][0] = 1; // 初始化}}for(int i = 1; i <= n; i ++){for(int j = 1; j <= m; j ++){for(int d = 0; d <= k; d ++){f[i][j][d][0] = (f[i][j][d][0] + f[i - 1][j][(d - a[i][j] + k) % k][1]) % mod;f[i][j][d][0] = (f[i][j][d][0] + f[i][j - 1][(d - a[i][j] + k) % k][1]) % mod;f[i][j][d][1] = (f[i][j][d][1] + f[i - 1][j][(d + a[i][j]) % k][0]) % mod;f[i][j][d][1] = (f[i][j][d][1] + f[i][j - 1][(d + a[i][j]) % k][0]) % mod;}}}ll ans = 0;for(int i = 1; i <= n; i ++){for(int j = 1; j <= m; j ++){(ans += f[i][j][0][1]) %= mod;}}cout << ans << '\n';
}signed main()
{ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);solve();return 0;
}

作业 / 拓展

[AGC043D] Merge Triplets

未完待续……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/12096.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于LLM的路由在专家混合应用:一种新颖的交易框架,该框架在夏普比率和总回报方面提升了超过25%

“LLM-Based Routing in Mixture of Experts: A Novel Framework for Trading” 论文地址&#xff1a;https://arxiv.org/pdf/2501.09636 摘要 随着深度学习和大语言模型&#xff08;LLMs&#xff09;的不断进步&#xff0c;混合专家&#xff08;MoE&#xff09;机制在股票投资…

Med-R2:基于循证医学的检索推理框架:提升大语言模型医疗问答能力的新方法

Med-R2 : Crafting Trustworthy LLM Physicians through Retrieval and Reasoning of Evidence-Based Medicine Med-R2框架Why - 这个研究要解决什么现实问题What - 核心发现或论点是什么How - 1. 前人研究的局限性How - 2. 你的创新方法/视角How - 3. 关键数据支持How - 4. 可…

【Blazor学习笔记】.NET Blazor学习笔记

我是大标题 我学习Blazor的顺序是基于Blazor University&#xff0c;然后实际内容不完全基于它&#xff0c;因为它的例子还是基于.NET Core 3.1做的&#xff0c;距离现在很遥远了。 截至本文撰写的时间&#xff0c;2025年&#xff0c;最新的.NET是.NET9了都&#xff0c;可能1…

C++ Primer 迭代器

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

2 [GitHub遭遇严重供应链投毒攻击]

近日&#xff0c;有黑客针对 Discord Top.gg 的GitHub 账户发起了供应链攻击&#xff0c;此次攻击导致账户密码、凭证和其他敏感信息被盗&#xff0c;同时也影响到了大量开发人员。 Checkmarx 在一份技术报告中提到&#xff0c;黑客在这次攻击中使用了多种TTP&#xff0c;其中…

【AudioClassificationModelZoo-Pytorch】基于Pytorch的声音事件检测分类系统

源码&#xff1a;https://github.com/Shybert-AI/AudioClassificationModelZoo-Pytorch 模型测试表 模型网络结构batch_sizeFLOPs(G)Params(M)特征提取方式数据集类别数量模型验证集性能EcapaTdnn1280.486.1melUrbanSound8K10accuracy0.974, precision0.972 recall0.967, F1-s…

基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息

之所以想写这一系列&#xff0c;是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器&#xff0c;但当时基于spring-boot 2.3.x&#xff0c;其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0&#xff0c;结果一看Spring Security也升级…

Jupyterlab和notebook修改文件的默认存放路径的方法

文章目录 1.缘由2.操作流程2.1找到默认的路径2.2创建配置文件2.3修改配置文件内容2.4注意事项 1.缘由 我自己使用jupyterlab的时候&#xff0c;打开是在这个浏览器上面打开的&#xff0c;但是这个打开的文件路径显示的是C盘上面路径&#xff0c;所以这个就很麻烦&#xff0c;因…

算法题(56):旋转链表

审题&#xff1a; 我们需要根据k的大小把链表向右移动对应次数&#xff0c;并返回移动后的链表的头结点指针 思路&#xff1a; 根据提示中的数据大小我们发现&#xff1a;k的值可以远大于节点数。 也就是说我们对链表的操作存在周期&#xff0c;如果k%len0&#xff0c;说明我们…

新月军事战略分析系统使用手册

新月人物传记&#xff1a; 人物传记之新月篇-CSDN博客 相关故事链接&#xff1a;星际智慧农业系统&#xff08;SAS&#xff09;&#xff0c;智慧农业的未来篇章-CSDN博客 “新月智能武器系统”CIWS&#xff0c;开启智能武器的新纪元-CSDN博客 “新月之智”智能战术头盔系统&…

金山打字游戏2010绿色版,Win7-11可用DxWnd完美运行

金山打字游戏2010绿色版&#xff0c;Win7-11可用DxWnd完美运行 链接&#xff1a;https://pan.xunlei.com/s/VOIAYCzmkbDfdASGJa_uLjquA1?pwd67vw# 进入游戏后&#xff0c;如果输入不了英文字母&#xff08;很可能是中文输入状态&#xff09;&#xff0c;就按一下“Shift”键…

99,[7] buuctf web [羊城杯2020]easyphp

进入靶场 <?php// 使用 scandir 函数扫描当前目录&#xff08;即脚本所在目录&#xff09;下的所有文件和文件夹// 该函数会返回一个包含目录下所有文件和文件夹名称的数组$files scandir(./); // 遍历扫描得到的文件和文件夹名称数组foreach($files as $file) {// 使用 …

Hot100之图论

200岛屿数量 题目 思路解析 把访问过的格子插上棋子 思想是先污染再治理&#xff0c;我们有一个inArea&#xff08;&#xff09;函数&#xff0c;是判断是否出界了 我们先dfs&#xff08;&#xff09;放各个方向遍历&#xff0c;然后我们再把这个位置标为0 我们岛屿是连着…

html中的表格属性以及合并操作

表格用table定义&#xff0c;标签标题用caption标签定义&#xff1b;用tr定义表格的若干行&#xff1b;用td定义若干个单元格&#xff1b;&#xff08;当单元格是表头时&#xff0c;用th标签定义&#xff09;&#xff08;th标签会略粗于td标签&#xff09; table的整体外观取决…

LabVIEW如何有效地进行数据采集?

数据采集&#xff08;DAQ&#xff09;是许多工程项目中的核心环节&#xff0c;无论是测试、监控还是控制系统&#xff0c;准确、高效的数据采集都是至关重要的。LabVIEW作为一个图形化编程环境&#xff0c;提供了丰富的功能来实现数据采集&#xff0c;确保数据的实时性与可靠性…

进阶数据结构——双向循环链表

目录 前言一、定义与结构二、特点与优势三、基本操作四、应用场景五、实现复杂度六、动态图解七、代码模版&#xff08;c&#xff09;八、经典例题九、总结结语 前言 这一期我们学习双向循环链表。双向循环链表不同于单链表&#xff0c;双向循环链表是一种特殊的数据结构&…

S4 HANA明确税金汇差科目(OBYY)

本文主要介绍在S4 HANA OP中明确税金汇差科目(OBYY)相关设置。具体请参照如下内容&#xff1a; 1. 明确税金汇差科目(OBYY) 以上配置点定义了在外币挂账时&#xff0c;当凭证抬头汇率和税金行项目汇率不一致时&#xff0c;造成的差异金额进入哪个科目。此类情况只发生在FB60/F…

在线知识库的构建策略提升组织信息管理效率与决策能力

内容概要 在线知识库作为现代企业信息管理的重要组成部分&#xff0c;具有显著的定义与重要性。它不仅为组织提供了一个集中存储与管理知识的平台&#xff0c;还能够有效提升信息检索的效率&#xff0c;促进知识的创新和利用。通过这样的知识库&#xff0c;企业可以更好地应对…

【汽车电子软件架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子软件架构&#xff1a;AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风&#xff0c;从概念与定义入手&#xff0c;希望读者能对AutoSAR架构有一个整体的认识&#xff0c;然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

DeepSeek-R1:通过强化学习激励大型语言模型(LLMs)的推理能力

摘要 我们推出了第一代推理模型&#xff1a;DeepSeek-R1-Zero和DeepSeek-R1。DeepSeek-R1-Zero是一个未经监督微调&#xff08;SFT&#xff09;作为初步步骤&#xff0c;而是通过大规模强化学习&#xff08;RL&#xff09;训练的模型&#xff0c;展现出卓越的推理能力。通过强…