差值 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.mzph.cn/pingmian/69025.shtml

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

相关文章

并行计算、分布式计算与云计算:概念剖析与对比研究(表格对比)

什么是并行计算&#xff1f;什么是分布计算&#xff1f;什么是云计算&#xff1f;我们如何更好理解这3个概念&#xff0c;我们采用概念之间的区别和联系的方式来理解&#xff0c;做到切实理解&#xff0c;深刻体会。 1、并行计算与分布式计算 并行计算、分布式计算都属于高性…

五. Redis 配置内容(详细配置说明)

五. Redis 配置内容(详细配置说明) 文章目录 五. Redis 配置内容(详细配置说明)1. Units 单位配置2. INCLUDES (包含)配置3. NETWORK (网络)配置3.1 bind(配置访问内容)3.2 protected-mode (保护模式)3.3 port(端口)配置3.4 timeout(客户端超时时间)配置3.5 tcp-keepalive()配置…

物业管理系统源码提升社区智能化管理效率与用户体验

内容概要 物业管理系统源码是一种针对社区管理需求而设计的软件解决方案&#xff0c;通过先进的智能化技术&#xff0c;使物业管理变得更加高效和人性化。随着城市化进程的加快&#xff0c;社区的管理复杂性不断增加&#xff0c;而这一系统的推出恰好为物业公司提供了极大的便…

springboot集成钉钉,发送钉钉日报

目录 1.说明 2.示例 3.总结 1.说明 学习地图 - 钉钉开放平台 在钉钉开放文档中可以查看有关日志相关的api&#xff0c;主要用到以下几个api&#xff1a; ①获取模板详情 ②获取用户发送日志的概要信息 ③获取日志接收人员列表 ④创建日志 发送日志时需要根据模板规定日志…

python算法和数据结构刷题[1]:数组、矩阵、字符串

一画图二伪代码三写代码 LeetCode必刷100题&#xff1a;一份来自面试官的算法地图&#xff08;题解持续更新中&#xff09;-CSDN博客 算法通关手册&#xff08;LeetCode&#xff09; | 算法通关手册&#xff08;LeetCode&#xff09; (itcharge.cn) 面试经典 150 题 - 学习计…

院校联合以项目驱动联合培养医工计算机AI人才路径探析

一、引言 1.1 研究背景与意义 在科技飞速发展的当下&#xff0c;医疗人工智能作为一个极具潜力的新兴领域&#xff0c;正深刻地改变着传统医疗模式。从疾病的早期诊断、个性化治疗方案的制定&#xff0c;到药物研发的加速&#xff0c;人工智能技术的应用极大地提升了医疗服务…

LeetCode 0045.跳跃游戏 II:贪心(柳暗花明又一村)

【LetMeFly】45.跳跃游戏 II&#xff1a;贪心&#xff08;柳暗花明又一村&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/jump-game-ii/ 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.18 对象数组:在NumPy中存储Python对象

2.18 对象数组&#xff1a;在NumPy中存储Python对象 目录 #mermaid-svg-shERrGOBuM2rBzeB {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-shERrGOBuM2rBzeB .error-icon{fill:#552222;}#mermaid-svg-shERrGOBuM2rB…

响应式编程与协程

响应式编程与协程的比较 响应式编程的弊端虚拟线程Java线程内核线程的局限性传统线程池的demo虚拟线程的demo 响应式编程的弊端 前面用了几篇文章介绍了响应式编程&#xff0c;它更多的使用少量线程实现线程间解耦和异步的作用&#xff0c;如线程的Reactor模型&#xff0c;主要…

python学opencv|读取图像(五十三)原理探索:使用cv.matchTemplate()函数实现最佳图像匹配

【1】引言 前序学习进程中&#xff0c;已经探索了使用cv.matchTemplate()函数实现最佳图像匹配的技巧&#xff0c;并且成功对两个目标进行了匹配。 相关文章链接为&#xff1a;python学opencv|读取图像&#xff08;五十二&#xff09;使用cv.matchTemplate()函数实现最佳图像…

javaEE-8.JVM(八股文系列)

目录 一.简介 二.JVM中的内存划分 JVM的内存划分图: 堆区:​编辑 栈区:​编辑 程序计数器&#xff1a;​编辑 元数据区&#xff1a;​编辑 经典笔试题&#xff1a; 三,JVM的类加载机制 1.加载: 2.验证: 3.准备: 4.解析: 5.初始化: 双亲委派模型 概念: JVM的类加…

【01】共识机制

BTF共识 拜占庭将军问题 拜占庭将军问题是一个共识问题 起源 Leslie Lamport在论文《The Byzantine Generals Problem》提出拜占庭将军问题。 核心描述 军中可能有叛徒&#xff0c;却要保证进攻一致&#xff0c;由此引申到计算领域&#xff0c;发展成了一种容错理论。随着…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型&#xff0c;用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说&#xff0c;N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意&#xff1a;这…

Python3 + Qt5:实现AJAX异步更新UI

使用 Python 和 Qt5 开发时异步加载数据的方法 在开发使用 Python 和 Qt5 的应用程序时&#xff0c;为了避免在加载数据时界面卡顿&#xff0c;可以采用异步加载的方式。以下是几种实现异步加载的方法&#xff1a; 1. 使用多线程&#xff08;QThread&#xff09; 通过将数据…

Tree Compass( Codeforces Round 934 (Div. 2) )

Tree Compass&#xff08; Codeforces Round 934 (Div. 2) &#xff09; You are given a tree with n n n vertices numbered 1 , 2 , … , n 1, 2, \ldots, n 1,2,…,n. Initially, all vertices are colored white. You can perform the following two-step operation: …

程序代码篇---项目目录结构HSV掩膜Opencv图像处理

文章目录 前言第一部分&#xff1a;项目目录结构第二部分&#xff1a;HSV提取HSV色调&#xff08;Hue&#xff09;含义取值范围 饱和度&#xff08;Saturation&#xff09;含义取值范围 亮度&#xff08;Value&#xff09;含义取值范围 第三部分&#xff1a;Opencv图像处理1. 读…

M. Triangle Construction

题目链接&#xff1a;Problem - 1906M - Codeforces 题目大意&#xff1a;给一个 n 边形&#xff0c; 每一个边上有a[ i ] 个点&#xff0c; 在此多边形上求可以连的三角形有多少个&#xff0c; 每个点只能用一次。 输入&#xff1a; 第一行是一个整数 N ( 3 ≤ N ≤ 200000…

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

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

python-UnitTest框架笔记

UnitTest框架的基本使用方法 UnitTest框架介绍 框架&#xff1a;framework&#xff0c;为了解决一类事情的功能集合 UnitTest框架&#xff1a;是python自带的单元测试框架 自带的&#xff0c;可以直接使用&#xff0c;不需要格外安装 测试人员用来做自动化测试&#xff0c;作…

EtherCAT主站IGH-- 49 -- 搭建xenomai系统及自己的IGH主站

EtherCAT主站IGH-- 49 -- 搭建xenomai系统及自己的IGH主站 0 Ubuntu18.04系统IGH博客、视频欣赏链接一 移植xenomai系统1,下载安装工具包2,下载linux内核及xenomai2.1,下载linux内核2.2,下载xenomai2.3,下载补丁ipipe2.4,解压缩包3,打补丁4,配置内核5,编译内核6,安装编译好的内…