浅谈一类转移带有数据结构操作特征的DP的优化
前言
有一类DP的转移是带有数据结构特征的,针对这一点,我们可以使用合适的数据结构来优化转移。
可能这么干说着不好理解,下面会给出两道题目来详细说明。
例子
1.【arc073f】Many Moves
题目大意:
你有两个整数 \(a\) 和 \(b\) 。
现在 \(n\) 个操作,依次执行,每次给你 \(xi\) ,你选择一个整数 \(y\) 变成 \(xi\) ,代价为 \(|xi-y|\) 。
求做完所有操作的最小代价。
\(n \leq 1e5,xi \leq 1e9\)
先写出DP状态,然后来一步一步优化它。
令\(f[i][j][k]\)表示,当前到了第 \(i\) 步,一个整数为\(x_j\),另一个整数为\(x_k\)的最小代价。
然后你发现当完成第 \(i\) 步时,一定有一个数是\(x_i\),那么,我们可以把状态优化一下:
令\(f[i][j]\)表示,当前到了第 \(i\) 步,一个整数为\(x_i\),另一个整数为\(x_j\)的最小代价。
把转移方程写出来:
在i相同时,第一个转移就是区间加,而第二个转移就是全局最小值之后单点赋值,这显然可以用线段树操作,复杂度\(\Theta (nlog_{2}n)\)。
2.「HB 省队互测 2019 Round1 Day1」轮回
题目链接
其实这题最难想的是 \(DP\) 的状态吧。
考虑,最后的结果一定是,有些动了,有一些没有动。而每一次的移动是任意插入的,那么,在排好 \(1\) 到 \(i-1\) 之后,如果我们知道最大的没有动的数,是不是说i的移动方向就确定了(就是移到最大的没有动的数的后面)?而这么设计状态是没有后效性的,于是有了这样一个想法:
令\(f[i][j]\)表示将序列中的元素 \([1,i]\)排好序,且最大的没有被移动的数是 \(j\) 的最小时间。
转移:
这里有一个技巧,因为转移的时候讨论的是pos的大小关系,所以为了方便区间操作,我们在实际使用线段树时的下标j实际上表示的是pos为j的值,然后每次转移就是前缀最小值,单点插入,然后前缀加,后缀加。
好像有点难理解,正好这道题是我为数不多的认真写了的题目(平常都是口胡),放一波代码。(请原谅本人丑陋的码风).
虽然写的丑,但由于复杂度是\(O(nlogn)\),比神仙学长 \(huyufeifei\) 的 \(O(n^2) std\)快了将近 \(30\) 倍,目前是 \(GUOJ\) 本题 \(rank1\) (欢迎各位来踩我大常数\(O(nlogn)\)代码)
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define inf 10000000000000
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
ll A,B;
#define ls(x) x<<1
#define rs(x) x<<1|1
void scan() {
cin>>n>>A>>B;
for(int i=1; i<=n; i++) {
int x;
scanf("%d",&x);
a[x]=i+1;
}
}
inline void push_up(ll p) {
ans[p]=min(ans[ls(p)],ans[rs(p)]);
}
void build(ll p,ll l,ll r) {
if(l==r) {
ans[p]=inf;
tag[p]=0;
return;
}
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
return;
}
inline void f(ll p,ll l,ll r,ll k) {
tag[p]=tag[p]+k;
ans[p]=ans[p]+k;
}
inline void push_down(ll p,ll l,ll r) {
ll mid=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]=0;
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k) {
if(nl<=l&&r<=nr) {
ans[p]+=k;
tag[p]+=k;
return ;
}
push_down(p,l,r);
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p) {
ll res=inf;
if(q_x<=l&&r<=q_y)return ans[p];
ll mid=(l+r)>>1;
push_down(p,l,r);
if(q_x<=mid)res=min(query(q_x,q_y,l,mid,ls(p)),res);
if(q_y>mid) res=min(res,query(q_x,q_y,mid+1,r,rs(p)));
return res;
}
void opc(ll cp,ll l,ll r,ll p,ll zhi) {
if(l==r&&cp==l) {
ans[p]=zhi;
return;
}
ll res=0;
ll mid=(l+r)>>1;
push_down(p,l,r);
if(cp<=mid) opc(cp,l,mid,ls(p),zhi);
if(cp>mid) opc(cp,mid+1,r,rs(p),zhi);
push_up(p);
return;
}
int main() {
ll a1,b,c,d,e,f;
scan();
build(1,1,n+1);
opc(1,1,n+1,1,0);
opc(a[1],1,n+1,1,0);
update(1,a[1]-1,1,n+1,1,B);
if(a[1]!=n+1)
update(a[1]+1,n+1,1,n+1,1,A);
// for(int j=1; j<=n+1; j++) {
// cout<<query(j,j,1,n+1,1)<<" ";
// }
// cout<<endl;
for(int i=2; i<=n; i++) {
ll anss=query(1,a[i]-1,1,n+1,1);
opc(a[i],1,n+1,1,anss);
update(1,a[i]-1,1,n+1,1,B);
if(a[i]!=n+1)
update(a[i]+1,n+1,1,n+1,1,A);
// for(int j=1; j<=n+1; j++)
// query(j,j,1,n+1,1);
}
cout<<query(1,n+1,1,n+1,1);
return 0;
}
附:思考题(我不会)
现在你有两个楼梯。有\(n\)个人排成一条队,每个人有一个通过楼梯的时间\(t_{i}\)。然后你每次可以让一个人从其中一个楼梯上楼(按排队的顺序)。因为楼梯过窄,后面的人不能超越前面的人,所以后面的人到达的时间一定大于等于前面的人的最慢到达时间(前面所有的人到达之后他才能到达)。定义一个人上楼的不爽值是他最终上楼所花的实际时间-\(t_i\)。请你最小化所有人的不爽值之和。
\(n \leq 1e5,t \leq 1e9\)。
完结撒花!