动态规划:区间DP 详解(三道例题c++实现)
关路灯
题目描述
某一村庄在一条路线上安装了 n n n 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为 1 m / s 1m/s 1m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位: m m m)、功率( W W W),老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
关灯的两种方式:假设向右为正方向,向左为反方向
- 我们一直往正方向前进,则 [i,j]范围内的灯会在这段路程中全部关闭,并且我们在结束一段区间的行进后位于右端点j上,在此过程中们通过某种方式得到了一直往正方向走是最优的。
- 我们首先往正方向前进,在[i,k]的范围内的灯会全部关闭,但是我们还没有到达 这段区间的终点j,我们在k位置时通过某种方法知道了再往正方向走不会产生最优解,则必须负方向走,以使得总的功耗代价是最小的。
可以推断出我们关灯有两种方式:要么一直往前,要么先往前,然后再折返。
那么判断我们需要在什么时候折返,什么时候往前呢?
我们设dp[i] [j] 的含义是:我们已经通过了[i,j]的区间,并且把这段区间的灯全部关闭了,这时所有路灯产生的总的功耗大小。
注意:在 【i,j】区间之外的所有的灯在行进的过程中都会产生功耗。
那么我们可以判断是终点位于 j 位置(一直往正方向走)所产生的总功耗最小,还是终点位于 i 位置(折返走)所产生的总功耗最小。
我们由此可以再引入一维: [ 0/1 ],设dp[i] [j] [0/1],0表示折返,终点在i位置;1表示一直往前,终点在j位置。
f[i] [j] [0]表示关掉i到j的灯后,老张站在 i 端点,f[i] [j] [1]表示关掉[i][j]的灯后,老张站在 j 端点。
状态转移方程:
d
p
[
i
]
[
j
]
[
0
]
=
m
i
n
(
d
p
[
i
+
1
]
[
j
]
[
0
]
+
(
p
o
s
[
i
+
1
]
−
p
o
s
[
i
]
)
∗
(
s
u
m
[
i
]
+
s
u
m
[
n
]
−
s
u
m
[
j
]
)
,
d
p
[
i
+
1
]
[
j
]
[
1
]
+
(
p
o
s
[
j
]
−
p
o
s
[
i
]
)
∗
(
s
u
m
[
i
]
+
s
u
m
[
n
]
−
s
u
m
[
j
]
)
)
d
p
[
i
]
[
j
]
[
1
]
=
m
i
n
(
d
p
[
i
]
[
j
−
1
]
[
0
]
+
(
p
o
s
[
j
]
−
p
o
s
[
i
]
)
∗
(
s
u
m
[
i
−
1
]
+
s
u
m
[
n
]
−
s
u
m
[
j
−
1
]
)
,
d
p
[
i
]
[
j
−
1
]
[
1
]
+
(
p
o
s
[
j
]
−
p
o
s
[
i
]
)
∗
(
s
u
m
[
i
−
1
]
+
s
u
m
[
n
]
−
s
u
m
[
j
−
1
]
)
)
dp[i][j][0]=min(dp[i+1][j][0]+(pos[i+1]-pos[i])*(sum[i]+sum[n]-sum[j]),\\ dp[i+1][j][1]+(pos[j]-pos[i])*(sum[i]+sum[n]-sum[j])) \\ dp[i][j][1]=min(dp[i][j-1][0]+(pos[j]-pos[i])*(sum[i-1]+sum[n]-sum[j-1]),\\ dp[i][j-1][1]+(pos[j]-pos[i])*(sum[i-1]+sum[n]-sum[j-1]))
dp[i][j][0]=min(dp[i+1][j][0]+(pos[i+1]−pos[i])∗(sum[i]+sum[n]−sum[j]),dp[i+1][j][1]+(pos[j]−pos[i])∗(sum[i]+sum[n]−sum[j]))dp[i][j][1]=min(dp[i][j−1][0]+(pos[j]−pos[i])∗(sum[i−1]+sum[n]−sum[j−1]),dp[i][j−1][1]+(pos[j]−pos[i])∗(sum[i−1]+sum[n]−sum[j−1]))
//TODO: Write code here int n,m; const int N=1e5+10; int pos[N],val[N],sum[N],dp[100][100][2]; signed main() { cin>>n>>m; sum[0]=0; memset(dp,INF,sizeof(dp)); for (int i=1;i<=n;i++) { cin>>pos[i]>>val[i]; sum[i]=sum[i-1]+val[i]; } dp[m][m][1]=dp[m][m][0]=0; //立刻被关 /* dp[i][j][0]: 路经i到j,并且[i,j]内的路灯全部被关,并且此时位置位于i位置 dp[i][j][1]: 此时位置位于j位置 */ for (int len=2;len<=n;len++) { for (int i=1;i<=n-len+1;i++) { int j=i+len-1; //如果要使得当前位于[i,j]的i位置,则应该从j往i走,则路径应该是 i->j->i dp[i][j][0]=min(dp[i+1][j][0]+(pos[i+1]-pos[i])*(sum[i]+sum[n]-sum[j]),//转移点位于dp[i+1][j][0],此时再走到i需要:路程长度:(pos[i+1]-pos[i])*[i,j]区间之外的路灯的功率 dp[i+1][j][1]+(pos[j]-pos[i])*(sum[i]+sum[n]-sum[j])); //转移点位于dp[i+1][j][1],此时再走到i需要:路程长度:(pos[j]-pos[i])*[i,j]区间之外的路灯的功率 dp[i][j][1]=min(dp[i][j-1][0]+(pos[j]-pos[i])*(sum[i-1]+sum[n]-sum[j-1]),//转移点位于dp[i][j-1][0],此时再走到j需要:路程长度:(pos[j]-pos[i])*[i,j-1]之外的路灯的功率 dp[i][j-1][1]+(pos[j]-pos[j-1])*(sum[i-1]+sum[n]-sum[j-1]));//转移点位于dp[i][j-1][1],此时走到j需要:路程长度:(pos[j]-pos[j-1])*[i,j-1]之外的路灯的功率 } } cout<<min(dp[1][n][0],dp[1][n][1]); #define one 1 return 0; }
合唱队
为了在即将到来的晚会上有更好的演出效果,作为 AAA 合唱队负责人的小 A 需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共 n n n 个人,第 i i i 个人的身高为 h i h_i hi 米( 1000 ≤ h i ≤ 2000 1000 \le h_i \le 2000 1000≤hi≤2000),并已知任何两个人的身高都不同。假定最终排出的队形是 A A A 个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中:
第一个人直接插入空的当前队形中。
对从第二个人开始的每个人,如果他比前面那个人高( h h h 较大),那么将他插入当前队形的最右边。如果他比前面那个人矮( h h h 较小),那么将他插入当前队形的最左边。
当 n n n 个人全部插入当前队形后便获得最终排出的队形。
例如,有 6 6 6 个人站成一个初始队形,身高依次为 1850 , 1900 , 1700 , 1650 , 1800 , 1750 1850, 1900, 1700, 1650, 1800, 1750 1850,1900,1700,1650,1800,1750,
那么小 A 会按以下步骤获得最终排出的队形:
1850 1850 1850。
1850 , 1900 1850, 1900 1850,1900,因为 1900 > 1850 1900 > 1850 1900>1850。
1700 , 1850 , 1900 1700, 1850, 1900 1700,1850,1900,因为 1700 < 1900 1700 < 1900 1700<1900。
1650 , 1700 , 1850 , 1900 1650, 1700, 1850, 1900 1650,1700,1850,1900,因为 1650 < 1700 1650 < 1700 1650<1700。
1650 , 1700 , 1850 , 1900 , 1800 1650, 1700, 1850, 1900, 1800 1650,1700,1850,1900,1800,因为 1800 > 1650 1800 > 1650 1800>1650。
1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750, 1650, 1700, 1850, 1900, 1800 1750,1650,1700,1850,1900,1800,因为 1750 < 1800 1750 < 1800 1750<1800。
因此,最终排出的队形是 1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750, 1650, 1700, 1850, 1900, 1800 1750,1650,1700,1850,1900,1800。
小 A 心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形。
请求出答案对 19650827 19650827 19650827 取模的值。
区间DP,这里只详细解释状态转移方程的由来:
对于形成 11 22 33 的几种情形: 设数字为最左端数字为 i ,最右端的数字为j,中间的数字为 i+1,或者 j-1
-
对于端点11的插入:
-
首先插入22,然后在插入33,此时再插入11,则11是根据33(j位置,最右端的值)而插入的。
-
首先插入33,然后再插入22,此时再插入11,则11是根据22(i+1位置的值)而插入的
-
-
对于右端点33的插入:
- 首先插入22,然后再插入11,此时再插入33,则33是根据11(i位置,最左端的值)而插入的。
- 首先插入11,然后再插入22,此时再插入33,则33是根据22(j-1位置的值)而插入的。
那么我们对于一个数字的插入,我们可以有以下的情况:
- nums[i] < nums[j] :当前 i 左端插入,它的依据是上一个数字nums[j]从右端插入
- nums[i] < nums[i+1]:当前 i 左端插入,它的依据是上一个数字nums[i+1]从左端插入(此时还没有nums[i])
- nums[j] > nums[i]:当前 j 右端插入,它的依据是上一个数字nums[i]从左端插入
- nums[j] > nums[j-1]当前 j 右端插入,它的依据是上一个数字nums[j-1]从右端插入(此时还没有nums[j])
我们便可以规定dp [i] [j] [0/1],表示 nums[i]或者nums[j],0:从左端插入,1:从右端插入
根据我们上面所归纳的这四种情况,我们便可以总结出dp的四种方式:
d
p
[
i
]
[
j
]
[
0
]
+
=
d
p
[
i
+
1
]
[
j
]
[
1
]
−
>
(
n
u
m
s
[
j
]
右端插入
)
d
p
[
i
]
[
j
]
[
0
]
+
=
d
p
[
i
+
1
]
[
j
]
[
0
]
−
>
(
n
u
m
s
[
i
+
1
]
左端插入
)
d
p
[
i
]
[
j
]
[
1
]
+
=
d
p
[
i
]
[
j
−
1
]
[
0
]
−
>
(
n
u
m
s
[
i
]
左端插入
)
d
p
[
i
]
[
j
]
[
1
]
+
=
d
p
[
i
]
[
j
−
1
]
[
1
]
−
>
(
n
u
m
s
[
j
−
1
]
右端插入
)
dp[i][j][0]+=dp[i+1][j][1] ->(nums[j]右端插入) \\ dp[i][j][0]+=dp[i+1][j][0] ->(nums[i+1]左端插入)\\ dp[i][j][1]+=dp[i][j-1][0] ->(nums[i]左端插入) \\ dp[i][j][1]+=dp[i][j-1][1] ->(nums[j-1]右端插入)\\
dp[i][j][0]+=dp[i+1][j][1]−>(nums[j]右端插入)dp[i][j][0]+=dp[i+1][j][0]−>(nums[i+1]左端插入)dp[i][j][1]+=dp[i][j−1][0]−>(nums[i]左端插入)dp[i][j][1]+=dp[i][j−1][1]−>(nums[j−1]右端插入)
这四种情况分别对应上面所归纳的四种情况: 1,2,3,4
#include <bits/stdc++.h> using namespace std; #if 1 #define int LL #endif using LL = long long; using DB = double; using PI = pair<int, int>; using PL = pair<LL, LL>; template<typename T> using v = vector<T>; constexpr auto INF = 0X3F3F3F3F; template<typename T1,typename T2> using umap = unordered_map<T1, T2>; #define ic std::ios::sync_with_stdio(false);std::cin.tie(nullptr) template <typename ConTainermap> void dbgumap(ConTainermap c); //output umap template <typename _Ty> void dbg( _Ty arr[],int n,int m=-1); inline int read(); //fast input inline void write(int x); //fast output //TODO: Write code here int n,m; const int N=2e3+10,mod=19650827; int a[N],dp[N][N][2]; signed main() { cin>>n; for (int i=1;i<=n;i++) { cin>>a[i]; dp[i][i][0]=1; } for (int len=2;len<=n;len++) { for (int i=1;i<=n-len+1;i++) { int j=i+len-1; /* 从左边进来: 当前a[i]一定小于上一个数:要么是a[i+1],要么是a[j]右端点 */ if (a[i]<a[i+1]) dp[i][j][0]+=dp[i+1][j][0]; if (a[i]<a[j]) dp[i][j][0]+=dp[i+1][j][1]; /* 从右边进来: 当前a[j]一定大于上一个数:要么是a[j-1],要么是a[i]左端点 */ if (a[j]>a[j-1]) dp[i][j][1]+=dp[i][j-1][1]; if (a[j]>a[i]) dp[i][j][1]+=dp[i][j-1][0]; dp[i][j][0]%=mod; dp[i][j][1]%=mod; } } cout<<(dp[1][n][0]+dp[1][n][1])%mod; #define one 1 return 0; } template <typename _Ty> void dbg(_Ty arr[],int n,int m) { #if one for (int i=1;i<=n;i++) { cout<<arr[i]<<' '; } #else if (m==-1){cout<<"please input m! ";return;} for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { cout<<*(arr[i]+j)<<' '; } cout<<endl; } #endif cout<<endl; } template <typename ConTainermap> void dbgumap(ConTainermap c) { for (auto& x:c) { cout<<"key:"<<x.first<<" val:"<<x.second<<endl; } } inline int read() { int x = 0, w = 1; char ch = 0; while (ch < '0' || ch > '9') { if (ch == '-') w = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + (ch - '0'); ch = getchar(); } return x * w; } inline void write(int x) { static int sta[35]; int top = 0; do { sta[top++] = x % 10, x /= 10; } while (x); while (top) putchar(sta[--top] + 48); }
能量项链
在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N N N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 m m m,尾标记为 r r r,后一颗能量珠的头标记为 r r r,尾标记为 n n n,则聚合后释放的能量为 m × r × n m \times r \times n m×r×n(Mars 单位),新产生的珠子的头标记为 m m m,尾标记为 n n n。
需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设 N = 4 N=4 N=4, 4 4 4 颗珠子的头标记与尾标记依次为 ( 2 , 3 ) ( 3 , 5 ) ( 5 , 10 ) ( 10 , 2 ) (2,3)(3,5)(5,10)(10,2) (2,3)(3,5)(5,10)(10,2)。我们用记号 ⊕ \oplus ⊕ 表示两颗珠子的聚合操作, ( j ⊕ k ) (j \oplus k) (j⊕k) 表示第 j , k j,k j,k 两颗珠子聚合后所释放的能量。则第 4 4 4 、 1 1 1 两颗珠子聚合后释放的能量为:
( 4 ⊕ 1 ) = 10 × 2 × 3 = 60 (4 \oplus 1)=10 \times 2 \times 3=60 (4⊕1)=10×2×3=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:
( ( 4 ⊕ 1 ) ⊕ 2 ) ⊕ 3 ) = 10 × 2 × 3 + 10 × 3 × 5 + 10 × 5 × 10 = 710 ((4 \oplus 1) \oplus 2) \oplus 3)=10 \times 2 \times 3+10 \times 3 \times 5+10 \times 5 \times 10=710 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。
类似于石子合并的思路。
这是一个项链环,我们首先把这个环拉成一条链条。
对于原始的数据: 2 3 5 10
拉成环后我们便可以得到:2 3 5 10 2 3 5 10
透漏一下,这道题的结果就是 10 2 3 5 10 这个环。
那么我们便可以枚举区间的长度,指定起点与终点,在区间种寻找最大值。
我们设dp[i] [j]表示以i为起点,j为终点所形成的珠子链的能量最大值
状态转移方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
s
u
m
[
i
]
∗
s
u
m
[
k
+
1
]
∗
s
u
m
[
j
+
1
]
)
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+sum[i]*sum[k+1]*sum[j+1])
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+sum[i]∗sum[k+1]∗sum[j+1])
注意sum里面的i k j 的参数:
假设 i=1,j=2,len=2,此时 k =1:
此时 i 与 k的值相同,j的值只比i,k 大一,如果不处理,这就形成了 1 * 1 *2 的形式,
但是应该是 1 * 2 * 3的形式,所以 k+1 ,j+1 需要处理。
//TODO: Write code here int n,m; const int N=4e3+10; int nums[N],dp[N][N]; signed main() { cin>>n; for (int i=1;i<=n;i++) { cin>>nums[i]; nums[i+n]=nums[i]; } for (int len=2;len<=n;len++) { for (int i=1;i+len<=2*n+1;i++) { int j=i+len-1; dp[i][j]=0; for (int k=i;k<j;k++) { //如果以i开始的链,则i的头标记不能动,依次乘以k+1中间值和j+1的尾标记 dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+nums[i]*nums[k+1]*nums[j+1]); } } } int maxn=0; for (int i=1;i<=n;i++) { maxn=max(maxn,dp[i][i+n-1]); } cout<<maxn; #define one 1 return 0; }
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209611.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效