决策单调性优化dp
好久没做过了
其有两种形式:
1.1d/1d dp\(f_i=\min(f_j+w(j+1,i))\)
它的特点是对于决策点\(a,b\),\(a<b\),如果\(a\)转移到\(c\)比\(b\)转移到\(c\)差,则以后\(a\)转移到\(d\)永远比\(b\)转移到\(d\)差
这说明决策点是单调递增的。
\(a<b<c<d,f_b+w(b+1,c)\leq f_a+w(a+1,c)\)
如果\(w(b+1,d)+w(a+1,c)\leq w(a+1,d)+w(b+1,c)\)
两者相加
\(f_b+w(b+1,d)\leq f_a+w(a+1,d)\)
证明成功。
引理:如果有\(w(a,b+1)+w(a+1,b)\geq w(a+1,b+1)+w(a,b)\)成立,则四边形不等式成立。
实际上,决策点也可以单调递减。
它有两种求法:
分治法:类似整体二分。
每次考虑更新中点。
如果要从数组\(c\)转移到\(d\),代码如下:
void fz(int *c,int *d,int l,int r,int a,int b){
if(l>r)
return;
int md=(l+r)/2,ans=l;
for(int i=l;i<=min(md,b);i++){
if(d[md]>c[i-1]+va(i,md)){
d[md]=c[i-1]+va(i,md);
ans=i;
}
fz(c,d,l,md-1,a,ans);
fz(c,d,md+1,r,ans,b);
}
\(ans\)事实上代表\([l,md]\)最大转移点
根据决策单调性的定义,右边\([md+1,r]\)区间的转移点肯定大于等于\(ans\),所以决策点被锁定到了\([l,md],[md+1,r]\)两个区间内。
ans的初值依题目而定。
分治法的优点:
1.好写好理解,不容易出错
2.如果\(va\)函数不容易求出,但是它移动一格(从\([l,r]\)移动到\([l,r-1],[l,r+1],[l+1,r],[l-1,r]\))容易求出(比如区间逆序对函数,直接做只能\(O(\sqrt{n})\),但是移动一格可以用bit维护)
那么\(va\)函数总移动次数是\(O(n\log_2n)\)的。
怎么移动?用类似莫队移动的方式
缺点:只能从\(c\)转移到\(d\),如果做1d/1d转移需要cdq分治,复杂度更高
二分+单调队列/栈法
以队列为例。假设决策点是单调递增的。
一个点\(i\)比\(j\)(\(i<j\))好的决策时间是区间\([1,k]\),可以二分。
维护可能成为决策点的单调队列,队列中相邻元素\(a_i<a_{i+1}\)且单调队列中\(a_i\)取代\(a_{i+1}\)的时间不下降。
在插入新的决策点时,插入到队列的尾部。
此时队尾可能有一些点不满足单调性,要删除。
所以要维护双端队列。
然后考虑队头的答案。
原来队头的答案事实上可能是不优的。
如果队列中第二个元素取代队头元素的时间小于当前决策点\(x\),则队头元素显然是不优的,可以删除。
重复以上过程,队头元素就是答案。
单调栈也类似。
维护可能成为决策点的单调栈,栈中元素编号从顶到底单调递减,且栈中\(a_i\)取代\(a_{i+1}\)的时间不下降。
在插入新的决策点时,插入到栈的顶部。
此时栈顶可能有一些点不满足单调性,要删除。
然后考虑栈顶的答案。
原来栈顶的答案事实上可能是不优的。
如果栈顶中第二个元素取代栈顶元素的时间小于当前决策点\(x\),则栈顶元素显然是不优的,可以删除。
重复以上过程,栈顶元素就是答案。
栈+二分代码如下:
int cl(int a,int b){
return f[a-1]+s[a]*b*b;
}
int ef(int x,int y){
int l=1,r=n,ans=n+1;
while(l<=r){
int md=(l+r)/2;
if(cl(x,md-t[x]+1)>=cl(y,md-t[y]+1)){
ans=md;
r=md-1;
}
else
l=md+1;
}
return ans;
}
for(int i=1;i<=n;i++){
while(st.size()>1&&ef(st[st.size()-2],st[st.size()-1])<=ef(st[st.size()-1],i))
st.pop_back();
st.push_back(i);
while(st.size()>1&&ef(st[st.size()-2],st[st.size()-1])<=i)
st.pop_back();
int x=st.back();
f[i]=cl(x,i-x+1);
}
\(cl(x,len)\)表示决策点为\(x\),区间长度为\(i-x+1\)的代价。
这份代码求的是dp最大值。
SMAWK算法:可以线性决策单调性,但是没什么用
如果\(f_i=\min(f_j+w(j+1,i))\)但是\(w(b+1,d)+w(a+1,c)\geq w(a+1,d)+w(b+1,c)\),决策是单调递减的
这是因为\(a<b<c<d,f_b+w(b+1,c)\geq f_a+w(a+1,c)\)
两者相加,\(f_b+w(b+1,c)+w(b+1,d)+w(a+1,c)\geq f_a+w(a+1,c)+w(a+1,d)+w(b+1,c)\)
\(f_b+w(b+1,d)\geq f_a+w(a+1,d)\)
证明成功。
决策点是否递增是判定使用单调栈/队列的因素。
2.2d/1d dp:形如\(f_{i,j}=\min(f_{i,k}+f_{k+1,j}+w(i,j))\)
如果dp的代价函数满足四边形不等式。
则\(s_{i,j-1}\leq s_{i,j}\leq s_{i+1,j}\)
转移分割点就从\(s_{i,j-1}\)转移到\(s_{i+1,j}\)
这样子均摊时间复杂度是\(O(n^2)\)的。
这是因为每次转移的\(s\)都在一条斜线上。
转移复杂度是\((s_{i,n}-s_{i-1,n-1})+(s_{i-1,n-1}-s_{i-2,n-2})+(s_{i-2,n-2}-s_{i-3,n-3})=s_{i,n}\)
这最多是\(O(n)\)的。
所以均摊时间复杂度是\(O(n^2)\)的。
事实上,如果dp满足决策单调性,那么它关于选择的段数是凸的。
Itst的博客中有证明。
考场上看出规律经常只能打表。
例题:
1.lg5574
容易列出方程\(f_{k,i}=\min(f_{k-1,j})+w(j+1,i)\)
可以证明\(w\)满足四边形不等式
但是事实上发现\(w\)很难快速求出,所以不能二分+队列。
但是它移动一格相当于查询一个区间内\(<v,>v\)的值的个数
用bit维护
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 25010
int n,k,bt[N],a[N],L,R,ans,f[30][N];
void ad(int x,int y){
for(;x<=n;x+=x&-x)
bt[x]+=y;
}
int qu(int x){
int ans=0;
for(;x;x-=x&-x)
ans+=bt[x];
return ans;
}
int va(int l,int r){
while(l<L){
L--;
ans+=qu(a[L]);
ad(a[L],1);
}
while(r>R){
R++;
ans+=R-L-qu(a[R]);
ad(a[R],1);
}
while(l>L){
ad(a[L],-1);
ans-=qu(a[L]);
L++;
}
while(r<R){
ad(a[R],-1);
ans-=R-L-qu(a[R]);
R--;
}
return ans;
}
void fz(int *c,int *d,int l,int r,int a,int b){
int md=(l+r)/2,po=a;
for(int i=a;i<=min(md-1,b);i++){
int vt=(md-i+1)*(md-i)/2-va(i,md);
if(d[md]>c[i-1]+vt){
d[md]=c[i-1]+vt;
po=i;
}
}
if(l==r){
return;
}
fz(c,d,l,md,a,po);
fz(c,d,md+1,r,po,b);
}
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
for(int i=1;i<=n;i++){
f[1][i]=f[1][i-1]+qu(a[i]);
ad(a[i],1);
}
ans=n*(n-1)/2-f[1][n];
L=1;
R=n;
for(int i=2;i<=k;i++){
memset(f[i],127,sizeof(f[i]));
fz(f[i-1],f[i],1,n,1,n);
}
printf("%lld\n",f[k][n]);
}
1.1 CF833B
又是模板题,没什么营养价值。
区间颜色个数用可持久化线段树/桶维护即可。
#include<bits/stdc++.h>
using namespace std;
#define N 35010
int n,k,a[N],L,R,ans,f[60][N],bc[N];
int va(int l,int r){
while(l<L){
L--;
if(!bc[a[L]])
ans++;
bc[a[L]]++;
}
while(r>R){
R++;
if(!bc[a[R]])
ans++;
bc[a[R]]++;
}
while(l>L){
bc[a[L]]--;
if(!bc[a[L]])
ans--;
L++;
}
while(r<R){
bc[a[R]]--;
if(!bc[a[R]])
ans--;
R--;
}
return ans;
}
void fz(int *c,int *d,int l,int r,int a,int b){
int md=(l+r)/2,po=a;
for(int i=a;i<=min(md,b);i++){
int vt=va(i,md);
if(d[md]<c[i-1]+vt){
d[md]=c[i-1]+vt;
po=i;
}
}
if(l==r)
return;
fz(c,d,l,md,a,po);
fz(c,d,md+1,r,po,b);
}
signed main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[1][i]=f[1][i-1];
if(!bc[a[i]])
f[1][i]++;
bc[a[i]]++;
}
ans=f[1][n];
L=1;
R=n;
for(int i=2;i<=k;i++)
fz(f[i-1],f[i],1,n,1,n);
printf("%d\n",f[k][n]);
}
1.2.CF868F
还是容易列出方程\(f_{k,i}=\min(f_{k-1,j})+w(j+1,i)\)
可以证明\(w\)满足四边形不等式。
但是事实上发现\(w\)很难快速求出,所以不能二分+队列。
移动一格相当于查询一个区间\(=v\)的值的个数
用桶维护,每次移动时加/减桶对应的那一位即可。
#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define int long long
int n,k,a[N],bc[N],l=1,r=1,va,f[20][N];
void ad(int x){
va+=bc[x];
bc[x]++;
}
void dl(int x){
bc[x]--;
va-=bc[x];
}
int qu(int x,int y){
while(l<x){
dl(a[l]);
l++;
}
while(l>x){
l--;
ad(a[l]);
}
while(r<y){
r++;
ad(a[r]);
}
while(r>y){
dl(a[r]);
r--;
}
return va;
}
void fz(int p,int l,int r,int x,int y){
if(l>r||x>y)return;
int o,md=(l+r)/2;
for(int i=x;i<=min(md,y);i++)
if(f[p-1][i]+qu(i+1,md)<f[p][md]){
f[p][md]=f[p-1][i]+qu(i+1,md);
o=i;
}
fz(p,l,md-1,x,o);
fz(p,md+1,r,o,y);
}
signed main(){
memset(f,32,sizeof(f));
f[0][0]=0;
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i];
bc[a[1]]++;
for(int i=1;i<=k;i++)
fz(i,1,n,0,n-1);
cout<<f[k][n];
}
2.诗人小G
模板题
3.IOI2014 holiday
容易发现,获取权值和移动的过程是独立的。
如果我们要获取权值\(S\),则可以考虑\(S\)的每个点,在遍历到\(S\)时获取\(S\)的该节点权值即可。
显然,对于固定的出发点,\(S\)是个区间。
贪心的想,遍历区间\([l,r]\)最小费用是\(\min(p-l,r-p)+r-l\)
暴力就是枚举区间\([l,r]\),求出最小费用\(d\),显然我们要获取的权值就是区间前\(k-d\)大。
用可持久化线段树求出。
考虑枚举\(l\),暴力枚举\(r\)不可取。
对于固定的\(l\),发现\(l\)对应的决策点\(f(l)\)是单调递增的(\(f(l)\leq f(l+1)\))
用经典的分治法求即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 120010
int n,p,d,a[N],s[2000010],aa,ct,b[N],t[N];
signed lc[2000010],rc[2000010],sz[2000010],rt[N];
void ins(signed &o,int p,int l,int r,int x,int v){
o=++ct;
sz[o]=sz[p]+1;
s[o]=s[p]+v;
lc[o]=lc[p];
rc[o]=rc[p];
if(l==r){
return;
}
int md=(l+r)/2;
if(x<=md)
ins(lc[o],lc[p],l,md,x,v);
else
ins(rc[o],rc[p],md+1,r,x,v);
}
int qu(int o,int p,int l,int r,int x){
if(l==r)
return min(x,1ll*(sz[o]-sz[p]))*t[l];
int md=(l+r)/2;
if(x>sz[rc[o]]-sz[rc[p]])
return qu(lc[o],lc[p],l,md,x-sz[rc[o]]+sz[rc[p]])+s[rc[o]]-s[rc[p]];
return qu(rc[o],rc[p],md+1,r,x);
}
void fz(int l,int r,int a,int b){
int ans,va=-1,md=(l+r)/2;
for(int i=a;i<=b;i++){
int t=d-(i-md)-min(i-p,p-md);
if(t>0)
t=qu(rt[i],rt[md-1],1,n,t);
else
t=0;
if(t>va){
ans=i;
va=t;
aa=max(aa,t);
}
}
if(l==r)
return;
fz(l,md,a,ans);
fz(md+1,r,ans,b);
}
signed main(){
scanf("%lld%lld%lld",&n,&p,&d);
p++;
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
t[i]=a[i];
}
sort(t+1,t+n+1);
for(int i=1;i<=n;i++)
b[i]=lower_bound(t+1,t+n+1,a[i])-t;
for(int i=1;i<=n;i++)
ins(rt[i],rt[i-1],1,n,b[i],a[i]);
fz(1,p,p,n);
printf("%lld",aa);
}
3.1 JOISC2019 蛋糕拼接
如果我们钦定了选择蛋糕的集合,考虑如何求出最大权值。
它的\(k\)值之和显然是不变的,我们要最小化\(C\)值。
考虑贪心的经典方法:排序,把\(C\)排序。
显然,选择排序后连续递增的\(C\)是最优的,费用为\(2(C_{\max}-C_{\min})\)
把\(C\)从小到大排序,则费用为\(\sum V-2(C_{r}-C_{l})\)
如果确定了\(l,r\),显然我们会选择最大的\(m-2\)个\(k\),用可持久化线段树求出。
还是考虑枚举\(l\),暴力枚举\(r\)不可取。
对于固定的\(l\),发现\(l\)对应的决策点\(f(l)\)是单调递增的(\(f(l)\leq f(l+1)\))
用经典的分治法求即可。
注意细节,最优决策点一开始要赋值成区间的尾部
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 200010
int n,m,s[N*31],aa=-1e18,ct,lc[N*31],rc[N*31],sz[N*31],rt[N];
struct no{
int v,c;
}a[N];
int operator <(no x,no y){
return x.c<y.c;
}
void ins(int &o,int p,int l,int r,int x){
o=++ct;
sz[o]=sz[p]+1;
s[o]=s[p]+x;
lc[o]=lc[p];
rc[o]=rc[p];
if(l==r){
return;
}
int md=(l+r)/2;
if(x<=md)
ins(lc[o],lc[p],l,md,x);
else
ins(rc[o],rc[p],md+1,r,x);
}
int qu(int o,int p,int l,int r,int x){
if(l==r)
return min(x,sz[o]-sz[p])*l;
int md=(l+r)/2;
if(x>sz[rc[o]]-sz[rc[p]])
return qu(lc[o],lc[p],l,md,x-sz[rc[o]]+sz[rc[p]])+s[rc[o]]-s[rc[p]];
return qu(rc[o],rc[p],md+1,r,x);
}
void fz(int l,int r,int L,int R){
int ans=R,va=-1e18,md=(l+r)/2;
for(int i=max(L,md);i<=R;i++)
if(i-md+1>=m){
int v=-(a[i].c-a[md].c)*2+a[i].v+qu(rt[i-1],rt[md],0,1e9,m-2)+a[md].v;
if(v>va){
ans=i;
va=v;
aa=max(aa,v);
}
}
if(l==r)
return;
fz(l,md,L,ans);
fz(md+1,r,ans,R);
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&a[i].v,&a[i].c);
}
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)
ins(rt[i],rt[i-1],0,1e9,a[i].v);
fz(1,n,1,n);
printf("%lld",aa);
}
4.Mowing Mischief
设\(f_i\)表示以\(i\)结尾的最小权值,则\(f_i=\min(f_j+(x_i-x_j)*(y_i-y_j)),g_j+1=g_i,x_i<x_j,y_i<y_j\)
\(g\)是\(lis\)数组,可以用bit求出。
求出\(g\)后,如果按照\(g_j+1=g_i\)的所有\(j\)按照\(x\)排序,则\(y\)是递减的。
发现这个dp可以决策单调性。
我们要解决\(x_i<x_j,y_i<y_j\)的问题
如果我们转移到节点\(i\)时,则合法的横坐标是一段区间,且左/右端点递增。
用线段树,把当前点挂在线段树的\(\log_2n\)个区间后,对每个区间运行决策单调性后更新答案即可。
我们并没有利用到左/右端点递增这个性质,利用这个性质可以得到更为优秀的做法。
考虑分块,如果用二分+队列法优化dp,则能够转移到当前点的是一个前缀,需要满足每个区间和块的交是块的前缀/后缀。
而且每个区间最多包含常数个块。
方法:用贪心,要让当前区间尽量大,以跨过尽量少的块。
对于当前要寻找的区间,找到最右的右端点,使得这个区间不严格包含任意一个区间。
这样子即可满足条件。
这个做法和双栈维护序列本质相同。
5.记忆的轮廓
#include<bits/stdc++.h>
using namespace std;
#define N 1510
int v[N*2],nxt[N*2],h[N],ec,n,m,p,d[N],vi[N],T;
#define db double
db a[N][N],g[N],s[N],f[N][N];
void add(int x,int y){
d[x]++;
v[++ec]=y;
nxt[ec]=h[x];
h[x]=ec;
}
void dfs(int x){
vi[x]=1;
g[x]=1;
for(int i=h[x];i;i=nxt[i]){
dfs(v[i]);
g[x]+=(1.0/d[x])*g[v[i]];
}
}
int main(){
scanf("%d",&T);
while(T--){
memset(h,0,sizeof(h));
ec=0;
memset(a,0,sizeof(a));
memset(d,0,sizeof(d));
memset(g,0,sizeof(g));
memset(s,0,sizeof(s));
memset(vi,0,sizeof(vi));
scanf("%d%d%d",&n,&m,&p);
for(int i=0;i<=n+2;i++)
for(int j=0;j<=n+2;j++)
f[i][j]=1e18;
for(int i=1;i<=m-n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<n;i++)
d[i]++;
for(int i=1;i<=n;i++)
if(!vi[i])
dfs(i);
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=nxt[j])
s[i]+=g[v[j]];
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
a[i][j]=a[i][j-1]*d[j-1]+d[j-1]+s[j-1];
f[1][1]=0;
for(int i=2;i<=p;i++){
for(int j=1;j<=n;j++)
for(int k=j-1;k;k--){
f[i][j]=min(f[i][j],f[i-1][k]+a[k][j]);
if(k-j>=40)
break;
}
}
printf("%.4lf\n",f[p][n]);
}
}
6.交与并
引理1:如果A包含B,则删除B对答案不会有影响
引理2:选的区间最多有2个。
考虑把剩下的区间按照左端点从小到大排序,则两个区间\(i,j,i<j\)的代价为\((r_j-l_i)*(r_i-l_j)\)
不难发现对于\(i\),对应的\(j\)最优决策点递增,用决策单调性优化即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 1000010
struct no{
int l,r;
}a[N],st[N];
int operator <(no x,no y){
return x.l<y.l||(x.l==y.l&&x.r>y.r);
}
int n,tp,ans;
void fz(int l,int r,int L,int R){
int md=(l+r)/2;
int po=max(L,md+1),va=-1e18;
for(int i=max(L,md+1);i<=R;i++)
if((st[i].r-st[md].l)*(st[md].r-st[i].l)>va){
va=(st[i].r-st[md].l)*(st[md].r-st[i].l);
ans=max(ans,va);
po=i;
}
if(l>=r)
return;
fz(l,md-1,L,po);
fz(md+1,r,po,R);
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&a[i].l,&a[i].r);
}
sort(a+1,a+n+1);
int p=0;
for(int i=1;i<=n;i++){
if(p>=a[i].r)
ans=max(ans,(st[tp].r-st[tp].l)*(a[i].r-a[i].l));
else{
st[++tp]=a[i];
p=a[i].r;
}
}
fz(1,tp,1,tp);
printf("%lld\n",ans);
}
7.柠檬
引理:每一段的首/尾一定是相同的,且当前段选择的一定是首/尾。
证明:把首/尾单成一段,答案肯定是更大的。
设\(f_i\)表示最后一段是\(i\)的最大价值。
枚举\(j\)使得\([j...i]\)是一段,\(f_i=\max(f_{j-1}+a_i(c_i-c_j+1)^2)\)
后面的用决策单调性二分栈维护。
8.jzoj5427
9.monster
伤害的最小值=和-抵挡伤害的最大值
引理:如果叠甲,则当护甲失效前不会再次叠甲
证明:否则把当前叠的甲移动到上次叠甲的位置答案不会更差。
考虑以未叠甲的1s和"叠了甲影响的区域"划分状态后dp,设\(f_{i,j}\)表示时间为\(i\),共叠了\(j\)甲的最小代价
设\(c_{i,j}\)表示第\(i\)秒叠\(j\)甲后能够抵挡的伤害,显然有转移方程:\(c_{i,j}=c_{i+1,j-1}+\max(a_i-j,0)\)
\(f\)的转移:
1.不叠甲,\(f_{i,j}=\min(f_{i,j},f_{i-1,j})\)
2.枚举哪里叠甲,\(f_{i,j}=\min(f_{k,j-i+k}+c_{k,i-k},f_{i,j})\)
时间复杂度\(O(n^3)\)
1转移不是瓶颈,考虑优化2的转移。
2转移事实上\(j-i\)是恒定的,在一条斜线上。\(f_{i,j}=\min(f_{i-k,j-k}+c_{i-k,k},f_{i,j})\)
显然\(i\leq j\)
枚举\(j-i=l\),设\(g_i=f_{i,i+l}\),\(w(a,b)=c_{a,b-a}\)则\(g_i=\min(g_{i-k}+w(i-k,i))\)
这个dp可以决策单调性
证明:事实上,我们要寻找\(c_{a,c-a}+c_{b,d-b}\)和\(c_{b,c-b}+c_{a,d-a}\)的关系
考虑转化模型:叠一次甲等于画一个三角形,\(a_i\)相当于一个位置为\(i\),高是\(a_i\)的柱子,要让被三角形覆盖的柱子面积最多
发现,\(c_{a,c-a}-c_{b,c-b}\)和\(c_{b,d-b}-c_{a,d-a}\)事实上都是一个高是\(b-a\)的矩形,但是\(c_{b,d-b}-c_{a,d-a}\)的底边/顶边更高。
所以\(c_{a,c-a}+c_{b,d-b}<c_{b,c-b}+c_{a,d-a}\),所以\(w(a,c)+w(b,d)<w(b,c)+w(a,d)\)
于是我们可以用决策单调性优化dp
10.Post加强版
又是裸题
11.环状邮局
做法1:钦定起始点,设当前起始点为0,其他点旋转一下。
然后做暴力dp
做法2:暴力dp用带权二分+二分队列优化
做法3:先求出一定选择\(0\)点的解。
由于决策单调性,所以其他钦定起点的路径与其是交错的(假设钦定\(0\)点的路径是\(0,a_1...a_{k-1}\),任意钦定起点的路径是\(x,b_1...b_{k-1}\),则不可能存在\(b_{i-1},b_i\)使得存在一个\(y\),\(a_{y-1}\leq b_{i-1},b_i\leq a_{y}\))
考虑找到\(a_{y}-a_{y-1}\)最小的一段,这一段显然有一个最优解的决策点
然后在这一段中钦定起点,运行\(O(nk\log_2n)\)的二分+队列dp
由抽屉原理,\(a_{y}-a_{y-1}\)最小的一段的点数不超过\(\frac{n}{k}\),时间复杂度\(O(n^2\log_2n)\)
做法4:求出一定选择\(0\)点的解后,找到\(a_{y}-a_{y-1}\)最小的一段,然后把这一段的起点设成\(0\)点,其他的点同时旋转。
建立平面直角坐标系:横坐标表示在原环上的坐标,纵坐标表示当前在第几个。
对于一个钦定选择\(x\)的解,画出一条折线,设路径是\(x,b_1...b_{k-1}\),画\((x,0),(b_1,1)......(b_{k-1},k-1)\)
路径不交错就代表折线不交。
我们现在求出了\(0\)开头的解,由路径交错,我们就知道了每个解所在的区间。
求出一个折线,可以用分治:一个点一个点的求出最优解。
这样子的时间复杂度是\(((a_1)+(a_2-a_1+1)+....)\log_2n\),是\(O(n\log_2n)\)的。
考虑在外层分治,找到起始点为中点的折线。
然后我们就可以确定\([l,md-1],[md+1,r]\)的折线的每个决策点的取值范围了。
时间复杂度\(O(n\log_2n(\log_2n+\log_2V))\)
12.uoj187
考虑dp,考虑以"完成接水果连续段"划分状态,每次添加未接水果连续段,接水果连续段补全序列。
设\(f_i\)表示\(i\)一定要接的最大价值,\(g_i\)表示\(i\)能够接到的
转移:\(f_i=\max(f_j+(j-i+1)^2+g_j)\),满足接\(j\)后能接\(i\),\(j\)是上一个连续段的末尾。
考虑优化:把每个点的(坐标,时间)放在平面上,一个点\(i\)能够转移到另一个点\(j\)要满足\(i\)在\(j\)引出的斜率为1,-1的直线构成的半平面上。
把坐标系逆时针旋转45度,则条件变成二维偏序。
把所有点按照\(x\)坐标排序,在转移时还要满足\(j\)的时间小于\(i\)的时间,但是\(j\)能够转移到\(i\)蕴含了这个条件,所以没有问题
\(g\)转移是简单的二维偏序问题,用bit维护前缀最大值即可。
\(f\)可以考虑把点数列划分成极大段,使得段内的x,y都递减。
如果\(i,j\)在同一段中,则\(j\)能够转移到\(i\)。
转移把括号拆开后是斜率优化形式,可以用半平面交解决。
要维护多个栈,用vector维护
当然也可以决策单调性,函数满足决策单调性,可以用决策单调性转移。
代码里用的斜率优化。
#include<bits/stdc++.h>
using namespace std;
#define N 500010
#define int long long
struct no{
int x,y,iy,id;
}a[N];
struct nn{
int k,b;
double x;
int f(int x){
return k*x+b;
}
};
int f[N],id[N],ct,bt[N],b[N],cc,le[N],g[N],ans;
void mod(int x,int y){
for(;x<=cc;x+=x&-x)
bt[x]=max(bt[x],y);
}
int qu(int x){
int ans=0;
for(;x;x-=x&-x)
ans=max(ans,bt[x]);
return ans;
}
double jd(nn x,nn y){
return 1.0*(x.b-y.b)/(y.k-x.k);
}
struct sg{
vector<nn>v;
void ins(nn x){
while(v.size()>1&&jd(x,v.back())>=v.back().x)
v.pop_back();
v.push_back(x);
if(v.size()>1){
v.back().x=jd(x,v[v.size()-2]);
}
}
int q(int x){
if(!v.size())
return -1e18;
while(v.size()>1&&v.back().x<=x)
v.pop_back();
return v.back().f(x);
}
}sg[N];
int n;
int operator <(no x,no y){
return x.x<y.x||(x.x==y.x&&x.y<y.y);
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
int c,d;
scanf("%lld%lld",&c,&d);
a[i]=(no){d-c,d+c,0,i};
b[++cc]=d+c;
}
id[1]=1;
ct=1;
for(int i=2;i<=n;i++){
if(a[i-1].x<=a[i].x&&a[i-1].y<=a[i].y)
id[i]=id[i-1];
else{
id[i]=++ct;
}
}
sort(a+1,a+n+1);
sort(b+1,b+cc+1);
cc=unique(b+1,b+cc+1)-b-1;
for(int i=1;i<=n;i++)
a[i].iy=lower_bound(b+1,b+cc+1,a[i].y)-b;
for(int i=1;i<=n;i++){
g[i]=qu(a[i].iy);
int x=a[i].id;
sg[id[x]].ins((nn){-2*x,g[i]+x*x-2*x});
f[i]=max(f[i],sg[id[x]].q(x)+x*x+2*x+1);
mod(a[i].iy,f[i]);
ans=max(ans,f[i]);
}
printf("%lld",ans);
}
13.shoes
计算代价可以把每双鞋按照平均值排序,则每双鞋装的柜子都是连续的。
可以发现一对鞋子的代价是分段的凸函数:在\((0,a_i)\)处是斜率为\(-1\)的直线,在\([a_i,b_i]\)处是斜率为\(-2\)的直线,在\((b_i,\inf)\)处是斜率为\(2\)的直线。
考虑以放置一对鞋子为过程划分状态。
每有一双鞋,设\(f(x)\)是鞋子放\(x\)位置的最优值,\(x\)就会加上下凸分段函数。
由中位数定律,把\([l,r]\)的所有鞋子从小到大排序,中间的鞋子的位置就是\([l,r]\)的鞋子的最优放置位置。
中位数可以用可持久化线段树找。
考虑每只鞋子的贡献:如果放置位置在左边,则ans+=它的位置-鞋柜的位置,否则and+=鞋柜的位置-它的位置。
这也可以用可持久化线段树维护。
发现这个函数满足四边形不等式,于是可以决策单调性。
用经典的分治/二分队列法即可。
#include<bits/stdc++.h>
#define N 5000010
using namespace std;
#define int long long
int lc[N],rc[N],s[N],c[N],ct,rt[N],o,n,m,f[2][N],ans;
struct no{
int x,y;
}a[N];
int operator <(no x,no y){return x.x+x.y<y.x+y.y;}
void ins(int &o,int p,int l,int r,int x){
o=++ct;
s[o]=s[p];
c[o]=c[p];
if(l==r){
s[o]+=l;
c[o]++;
return;
}
int md=(l+r)/2;
if(x<=md){
ins(lc[o],lc[p],l,md,x);
rc[o]=rc[p];
}
else{
ins(rc[o],rc[p],md+1,r,x);
lc[o]=lc[p];
}
s[o]=s[lc[o]]+s[rc[o]];
c[o]=c[lc[o]]+c[rc[o]];
}
void q(int o,int p,int l,int r,int x){
if(l==r){
ans+=-x*l;
return;
}
int md=(l+r)/2;
if(c[lc[o]]-c[lc[p]]>c[rc[o]]+x-c[rc[p]]){
ans+=s[rc[o]]-s[rc[p]];
q(lc[o],lc[p],l,md,x+c[rc[o]]-c[rc[p]]);
}
else if(c[lc[o]]-c[lc[p]]<c[rc[o]]+x-c[rc[p]]){
ans+=-s[lc[o]]+s[lc[p]];
q(rc[o],rc[p],md+1,r,x-c[lc[o]]+c[lc[p]]);
}
else ans+=s[rc[o]]-s[rc[p]]-s[lc[o]]+s[lc[p]];
}
void dfs(int l,int r,int a,int b){
int md=(l+r)/2;
if(l>r)return;
int p;
for(int j=a;j<=min(md-1,b);j++){
ans=0;
q(rt[md],rt[j],1,2000000002,0);
if(f[o][md]>f[1-o][j]+ans){
f[o][md]=f[1-o][j]+ans;
p=j;
}
}
dfs(l,md-1,a,p);
dfs(md+1,r,p,b);
}
signed main(){
freopen("shoes.in","r",stdin);
freopen("shoes.out","w",stdout);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y;
a[i].x+=1000000001;
a[i].y+=1000000001;
if(a[i].x>a[i].y)swap(a[i].x,a[i].y);
}
sort(a+1,a+n+1);
for(int i=1;i<=n;i++){
ins(rt[i],rt[i-1],1,2000000002,a[i].x);
ins(rt[i],rt[i],1,2000000002,a[i].y);
}
memset(f,127,sizeof(f));
f[0][0]=0;
for(int i=1;i<=m;i++){
o=!o;
for(int j=1;j<=n;j++)
f[o][j]=1e17;
dfs(i,n,i-1,n);
}
cout<<f[o][n];
}
14.lg6932
和loj535异常相似
类似14.1一样构造单调点集,则关于每个左下角对应的右上角是单调的。
用分治法求解,时间复杂度\(O(n\log_2^2n)\)
注意细节,在更新时有一维满足要求即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
#define N 500010
struct no{
int x,y;
}a[N],b[N];
int operator <(no x,no y){
return x.x<y.x||(x.x==y.x&&x.y<y.y);
}
int n,m,i1[N],i2[N],t1=1,t2=1,aa;
void fz(int l,int r,int L,int R){
int ans=L,va=-1e18,md=(l+r)/2;
for(int i=L;i<=R;i++)
if(b[i2[i]].x>=a[i1[md]].x){
int v=(b[i2[i]].x-a[i1[md]].x)*(b[i2[i]].y-a[i1[md]].y);
if(v>va){
ans=i;
va=v;
aa=max(aa,v);
}
}
if(l==r)
return;
fz(l,md,L,ans);
fz(md+1,r,ans,R);
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld%lld",&a[i].x,&a[i].y);
sort(a+1,a+n+1);
i1[1]=1;
for(int i=2;i<=n;i++){
if(a[i].y<a[i1[t1]].y)
i1[++t1]=i;
}
for(int i=1;i<=m;i++)
scanf("%lld%lld",&b[i].x,&b[i].y);
sort(b+1,b+m+1);
i2[1]=m;
for(int i=m-1;i;i--)
if(b[i].y>b[i2[t2]].y)
i2[++t2]=i;
reverse(i2+1,i2+t2+1);
fz(1,t1,1,t2);
printf("%lld\n",aa);
}
14.1 loj535
原题JOI final 2013 E
第一部分:最优交换次数是逆序对数。
时间复杂度\(O(n^3)\)
第二部分:用容斥,求出交换一次后减少最多的逆序对。
把\((i,a_i)\)投射到平面上,则交换\(a_i,a_j\)一次后减少的是以\((i,a_i)\)为左上角,\((j,a_j)\)为右下角的矩形内的点的个数。
第三部分:构造单调点集。
如果存在两个点\((j,a_j),(k,a_k),j<k,a_j<a_k\),则\(j\)构建矩形显然是没有\(k\)优的。
单调点集的求法:维护栈,按照横坐标从小到大排序,如果当前点的纵坐标也单调则插入栈。
第四部分:决策单调性
一个矩形左上角对应的最优右下角是单调的,而且由于已经构造了单调点集,一个矩形内包含的点数就是在bit上查询\(a_i,a_j\)的值。
用分治和前面所提到的类似莫队的方法维护,时间复杂度\(O(n\log_2^2n)\)
第五部分:逆向思维
更为优秀的做法:把所有单调点集按照横坐标排序,枚举左上角标号为\(i\),右下角标号为\(j\)的点,投射到平面\((i,j)\)。
中间一个点会对平面上的一个矩形的点的逆序对数作区间加法。
用扫描线+线段树维护矩形,时间复杂度\(O(n\log_2n)\)
15.lg6918
设\(d1_i\)表示\(s->i\)的距离,\(d2_i\)表示\(i->s\)的距离,可以在原图/反图上用dij求出。
点集的代价就是\(\sum_{i\in S}\sum_{j\in S}[i\neq j]d1_i+d2_j\)。
每个元素被计算\(card(S)-1\)次,所以代价是\(\sum_{i\in S}(card(S)-1)(d1_i+d2_i)\)
根据排序不等式,\(d1_i+d2_i\)越大,对应的集合大小就越小
把\(a_i=d1_i+d2_i\)从小到大排序,选择的一定是连续的区间。
考虑dp,设\(f_{c,i}\)表示划分了\(c\)个,包含排序后前\(i\)个元素,有转移\(f_{c,i}=\min(f_{c-1,j}+(i-j)(s_i-s_j))\),\(s\)是\(a\)的前缀和数组
容易知道是正确的。
发现这个dp有决策单调性,于是能用二维决策单调性优化。
16.lg5897
\(C\)和\(R\)差一个数量级,考虑对\(C\)建立线段树。
肯定要枚举\([l,r]\),我们要快速知道\(l,r\)中间的决策点\(md\)。
设函数\(p(l,r)\)表示\([l,r]\)的决策点,则\(p(l-1,r)\leq p(l,r)\leq p(l,r+1)\)
这是因为如果决策点交叉,则调整一下更优。
用二维决策单调性求出\(p\)。
然而这样子在题目的空间下会MLE。
考虑阈值法,对线段树上面的节点暴力不优,考虑对下面的节点暴力。
考虑如何暴力,设\(f_i\)表示转移到当前节点的距离最小值,用滚动数组。
设\(g_i\)表示原数组,则\(f_i=\min(g_i+|dis_i-dis_j|)\)
拆开绝对值后维护最小值即可。
17.数据分块鸡
思维量主要在费用函数的计算上。
我们希望在\(O(\log_2n)\)时间或者更低计算费用函数。
设当前分块区间\([L,R]\),查询是\([l,r]\),设分块区间中点为\(md\)
1.询问是\([L,R]\)的真子集,\(L\leq l\leq r\leq r\),代价为\((R-L)+l-r\)
2.\([L,R]\)是询问的真子集,则\(l\leq L\)且\(R\leq r\),代价为\(1\)
3.询问和\([L,R]\)交于一个前缀,且前缀长度不超过\([L,R]\)的一半,代价为\(r-(L)\)
4.询问和\([L,R]\)交于一个前缀,且前缀长度超过\([L,R]\)的一半,代价为\((R)-r\)
5.询问和\([L,R]\)交于一个后缀,且后缀长度不超过\([L,R]\)的一半,代价为\((R)-l\)
6.询问和\([L,R]\)交于一个后缀,且后缀长度超过\([L,R]\)的一半,代价为\(l-(L)\)
当分块区间和查询重合时,贡献会被计算\(2\)次,但是计算代价一次是\(0\)一次是\(1\),所以也不会影响答案。
所有情况事实上相当于平面上的一个矩形,要知道点的数量,横/纵坐标的和。
显然可以用可持久化线段树维护
18.loj6039
\(C\)非常小。
如果所有的\(C\)相同怎么做?肯定全选择最大的。
设\(f_{i,j}\)表示费用为\(i\),选\(j\)个的最大答案,则显然\(f_i\)是下凸函数。
如果有不同的\(C\),可以把所有物品按照\(C\)分组,然后把每个下凸函数合并。
枚举当前的数的模\(C\)的值,然后对每个模意义下相等的物品决策单调性。
下凸函数合并可以决策单调性:我们要求\(c_{i+j}=\max(a_i+b_j)\),\(a_i\)对应的最优\(b_j\)是单调的。
用经典的分治法合并即可。
边界很玄学
#include<bits/stdc++.h>
using namespace std;
#define N 1000010
#define int long long
int n,k,mx,nw[N],la[N],s2[N],t1,s1[N],t2;
vector<int>g[N];
void fz(int l,int r,int L,int R,int po,int v){
int ans,va=-1e18,md=(l+r)/2;
for(int i=L;i<=min(md,R);i++)
if(s1[i]+s2[md-i]>va){
va=s1[i]+s2[md-i];
ans=i;
}
nw[md*v+po]=max(nw[md*v+po],va);
if(l>=r)
return;
fz(l,md-1,L,ans,po,v);
fz(md+1,r,ans,R,po,v);
}
signed main(){
//freopen("jewelry.in","r",stdin);
//freopen("jewelry.out","w",stdout);
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++){
int c,v;
scanf("%lld%lld",&c,&v);
g[c].push_back(v);
mx=max(mx,c);
}
for(int i=1;i<=mx;i++){
sort(g[i].begin(),g[i].end());
reverse(g[i].begin(),g[i].end());
for(int j=1;j<g[i].size();j++)
g[i][j]+=g[i][j-1];
}
for(int j=0;j<=k;j++)
nw[j]=la[j]=-1e18;
nw[0]=0;
for(int i=1;i<=mx;i++){
for(int j=0;j<=k;j++)
la[j]=nw[j];
for(int j=0;j<=k;j++)
nw[j]=-1e18;
for(int j=0;j<i;j++){
t1=t2=s1[0]=s2[0]=0;
for(int l=j;l<=k;l+=i)
s1[t1++]=la[l];
s2[0]=0;
t2++;
for(int l=0;l<g[i].size();l++)
s2[t2++]=g[i][l];
for(int l=t2;l<t1;l++)
s2[l]=s2[l-1];
fz(0,t1-1,0,t1-1,j,i);
}
}
for(int i=1;i<=k;i++){
nw[i]=max(nw[i],nw[i-1]);
printf("%lld ",nw[i]);
}
}
这道题中如果\(a\)和\(b\)合并能够决策单调性,则\(a,b\)只要一个下凸即可。
总结:
决策单调性用于求一个有序的数列。
分治法的函数移动是连续的。
二分+队列法可以支持向末尾插入元素/查询最优解,但是要求代价函数能够被快速算出。
只要dp方程满足四边形不等式通常就能决策单调性。
满足决策单调性的方程通常可以带权二分。
带环的决策单调性可以用分治解决。
两个凹函数/一个凹一个非凹函数的\((\max,+)\)卷积可以用决策单调性快速计算。
事实上,决策单调性不重要,列出dp方程,计算代价函数才是重要的。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步