YbtOJ 「动态规划」 第2章 数据结构优化 DP
数据结构优化dp
考虑如下dp方程: $f(x) = min { f(i)+w(i) } \ (\ 1\le x \le i\ and\ h(i) < h(x)\ ) $
可以将\(h(i)\)先行离散化(因为显然用不到\(h(i)\)的值) 再以\(h(i)\)作为下标 将\(f[i]+w[i]\)作为值塞进某数据结构中
树状数组 二维偏序
定义了一个$ a[j] \le a[i]\ and\ b[j] \le b[i]\ $的关系 而且题中根据y上升再x上升给出
所以在推到第i个点的时候 前i-1个点的y坐标显然是要满足$b[j] \le b[i] $的关系的 所以考虑将x这一维度用树状数组来维护
以x[i]的值为下标建立树状数组 推到i这个点的时候先查询小于x[i]的点的个数 再把x[i]的值作为下标 1作为值塞进树状数组中 反复进行查询
在大部分题目中 二维偏序的实现方式是:
1.首先先按照某一个关键字排序 即满足了第一维度
2.其次以第二个关键字作为下标 dp值作为值塞进树状数组中 每次查询的时候以当前这个点的第二关键字作为查询下标 查询比这个大/小的所有dp值的和 满足了第二维度
[例题1] P7302 [NOI1998] 免费的馅饼
[题目描述]
SERKOI 最新推出了一种叫做“免费馅饼”的游戏:游戏在一个舞台上进行。舞台的宽度为 \(w\) 格(从左到右依次用 \(1\) 到 \(w\) 编号),游戏者占一格。开始时游戏者可以站在舞台的任意位置,手里拿着一个托盘。下图为天幕的高度为 \(4\) 格时某一个时刻游戏者接馅饼的情景。
游戏开始后,从舞台天幕顶端的格子中不断出现馅饼并垂直下落。游戏者左右移动去接馅饼。游戏者每秒可以向左或向右移动一格或两格,也可以站在原地不动。
当馅饼在某一时刻恰好到达游戏者所在的格子中,游戏者就收集到了这块馅饼。当馅饼落在一个游戏者不在的格子里时该馅饼就消失。
写一个程序,帮助我们的游戏者收集馅饼,使得所收集馅饼的分数之和最大。
[输入格式]
第一行是用空格隔开的两个正整数,分别给出了舞台的宽度 \(w\) 和馅饼的个数 \(n\)。
接下来 \(n\) 行,每一行给出了一块馅饼的信息。
由三个正整数组成,分别表示了每个馅饼落到舞台上的时刻 \(t_i\),掉到舞台上的格子的编号 \(p_i\),以及分值 \(v_i\)。
游戏开始时刻为 \(0\)。
输入文件中同一行相邻两项之间用一个空格隔开。
输入数据中可能存在两个馅饼的 \(t_i\) 和 \(p_i\) 都一样。
[输出格式]
一个数,表示游戏者获得的最大总得分。
[算法分析]
树状数组优化dp
首先设\(dp[i]\)表示对于前i个馅饼 而且接住了第i个的最大贡献
可行转移的条件:\(dp[i]=max\{ dp[j]+v[i] \}(abs(pi-pj) <= 2 * (ti-tj)\) 在这个条件下\(dp[j]\)可以成为\(dp[i]\)的合法前继
考虑拆开转移柿子:\(-2 * ( ti-tj ) \le pi-pj \le 2 * ( ti-tj )\)
\(2tj−pj≤2ti−pi\) 且 \(2tj+pj≤2ti+pi\) 将这两个值分别设置成结构体中关键字x,y
考虑先将x排序 这样满足了第一维 再根据y排序(需要离散化) 将y的下标塞进树状数组中 每次用当前i的y坐标进行查询 更新dp值 再将这次的dp值用y作为下标塞进去
树状数组的上界要小心考虑
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n , w , f[N] , ans , b[N] , c[N] , sz;
struct node
{
int t , p , v , x , y;
bool operator < ( const node &a ) const { return x < a.x; }
}t[N];
inline int lowbit ( const int &x ) { return x & (-x); }
inline void add ( int x , int val )
{
for ( int i = x ; i <= sz ; i += lowbit(i) )
c[i] = max ( c[i] , val );
}
inline int query ( int x )
{
int res = 0;
for ( int i = x ; i ; i -= lowbit(i) ) res = max ( res , c[i] );
return res;
}
signed main ()
{
w = read () , n = read(); //舞台的宽度和馅饼的个数
for ( int i = 1 ; i <= n ; i ++ )
{
t[i].t = read() , t[i].p = read() , t[i].v = read();//落下来的时刻 掉到舞台上格子的编号 分值v
t[i].x = 2 * t[i].t + t[i].p;//分别是能到这里的左右初始位置极值
t[i].y = 2 * t[i].t - t[i].p;
b[i] = t[i].y;
}
sort ( t + 1 , t + n + 1 );//先将x作为第一关键字排序 显然 在推到i的时候 前面所有的值的x坐标都比i的x坐标小
sort ( b + 1 , b + n + 1 );
sz = unique ( b + 1 , b + n + 1 ) - b - 1;
for ( int i = 1 ; i <= n ; i ++ )//将y离散化 以y作为树状数组下标
t[i].y = lower_bound ( b + 1 , b + sz + 1 , t[i].y ) - b;
for ( int i = 1 ; i <= n ; i ++ )
{
f[i] = query ( t[i].y ) + t[i].v;
add ( t[i].y , f[i] );//每一次查询之后需要将新的y值作为下标 插入数组中
ans = max ( ans , f[i] );//在dp同时统计答案
}
printf ( "%d\n" , ans );
return 0;
}
[例题2] P3287 [SCOI2014]方伯伯的玉米田
[题目描述]
方伯伯在自己的农田边散步,他突然发现田里的一排玉米非常的不美。这排玉米一共有 \(N\) 株,它们的高度参差不齐。方伯伯认为单调不下降序列很美,所以他决定先把一些玉米拔高,再把破坏美感的玉米拔除掉,使得剩下的玉米的高度构成一个单调不下降序列。方伯伯可以选择一个区间,把这个区间的玉米全部拔高 \(1\) 单位高度,他可以进行最多 \(K\) 次这样的操作。拔玉米则可以随意选择一个集合的玉米拔掉。问能最多剩多少株玉米,来构成一排美丽的玉米。
[输入格式]
第一行包含两个整数 \(n, K\),分别表示这排玉米的数目以及最多可进行多少次操作。第二行包含 \(n\) 个整数,第 \(i\) 个数表示这排玉米,从左到右第 \(i\) 株玉米的高度 \(a_i\)。
[输出格式]
输出一个整数,最多剩下的玉米数。
[算法分析]
二维树状数组
首先考虑每一次要提升[i,n]这一个区间 拔高的右端点必然是n 且最终的LIS必定包括这个a[i] 不然纯属出力不讨好
\(dp[i][j]\)表示拔高了\(j\)次 每次拔高的起点都不超过\(i\) 以\(i\)为结尾的最上升子序列有多长
\(dp[i][j] = max (dp[x][y] + 1) (x \le i , y \le j , a[x] + y \le a[i] + j)\)
解释条件:
- i只可能在前面的区间内转移过来
- 前面区间内的拔高次数必须要比j小
- 这个点的拔高次数与高度的加和必然要大于上一个点 否则接不到后面去
第一个条件天然满足 后两个条件作为下标限制 dp值作为值塞进树状数组中实现转移
注意j一维需要倒序枚举 保证了在使用当前值的时候不会查询到前一层的dp值
树状数组下标必须从1开始 但是这题可能有0 所以所有拉伸次数默认加一 这个"拉伸次数"只是针对所有第二个下标的(即存进去的时候为了方便整体加一 其他地方用到j的时候还是按照原来的j来用)
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
const int M = 5e2 + 5;
const int MAX = 5505;
int n , k , f[N][M] , ans , a[N] , c[N][M];
int lowbit (const int x) { return x & (-x); }
void upd ( const int x , const int y , const int val )
{
for (int i = x ; i <= MAX ; i += lowbit(i))
for ( int j = y ; j <= k + 1 ; j += lowbit(j) )
c[i][j] = max ( c[i][j] , val );
}
int query (const int x , const int y)
{
int ans = 0;
for (int i = x ; i ; i -= lowbit(i))
for (int j = y ; j ; j -= lowbit(j))
ans = max(ans , c[i][j]);
return ans;
}
signed main ()
{
n = read() , k = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = k ; j >= 0 ; j -- )
{
f[i][j] = query ( a[i] + j , j + 1 ) + 1;
ans = max ( ans , f[i][j] );
upd ( a[i] + j , j + 1 , f[i][j] );//所有次数小于j+1和所有高度小于a[i]+j的值都会被查询到
}
//query的是所有高度加和满足条件且拉伸次数也满足条件的dp值的最大值
printf ( "%d\n" , ans );
return 0;
}