首页 > 其他分享 >一些 dp 的优化方法 Part 2

一些 dp 的优化方法 Part 2

时间:2022-12-27 17:55:08浏览次数:37  
标签:le cost int mid Part 单调 优化 dp

决策单调性:分治:2D/1D

决策单调性分治常见于 2D / 1D 动态规划,其中一维是 转移层数,且从一层 仅转移至下一层

注意:下列讨论全部基于具有 决策单调性 的动态规划。

四边形不等式

通常情况下,决策单调性体现在其中的一维。

而另一位是层,假设当前要从上一层 \(f\),转移到这一层 \(g\)。

即 \(i\) 的最小的最优决策点为 \(p_i\)。

满足决策单调性就要满足 \(\forall i<j,p_i\le p_j\)。

设 \(f_i=f_j+w(j,i)\) 那么 \(w\) 满足四边形不等式:包含劣于相交

对于四边形不等式是否满足一般靠猜测

我们 不证明 决策单调性,仅进行 猜测

算法简介

一层一层 \(dp\)。

考虑暴力求出 \(mid\) 的最优决策点 \(p_{mid}\)。

由于相决策单调性,\(p_{1\sim mid-1}\le p_{mid}\) 和 \(p_{mid+1,n}>p_{mid}\)。

于是分治记四个参数 \(L,R,l,r\),表示 \(\in [L,R]\) 的最优决策点还需要确定,最优决策点位于 \([l,r]\)。

每次分裂成 \([L,mid-1,l,p_{mid}]\) 和 \([mid+1,R,p_{mid},r]\) 即可。

时间复杂度转移一层 \(\mathcal{O}(n\log n)\)。

总时间复杂度 \(\mathcal{O}(k n\log n)\)。

技巧:贡献难算

有时我们无法直接计算两个位置之间的贡献,但若 \(l\to r\) 的权值能够快速地在 \(\mathcal{O}(v)\) 的时间内扩展到 \((l±1)→(r±1)\),决策单调性仍然可以做到 \(\mathcal{O}(nv\log n)\),即端点移动距离总和级别是线性对数。类比莫队,维护当前贡献区间的左右端点 \([l,r]\),如果要查询某个区间的贡献,直接左右端点暴力跳到该区间。

例题 \(1\):\(\text{CF868F}\)

给定一个序列 \(a\),要把它分成 \(k\) 个子段。每个子段的费用是其中相同元素的对数。求所有子段的费用之和的最小值。

\(\texttt{data range}\):\(2 \leq n \leq 10^5\),\(2 \leq k \leq min(n,20)\),\(1 \leq a_i \leq n\) 。

\(dp_{i,j}\) 表示考虑到 \(a_i\) 分成了 \(j\) 个段,最小费用。

\(dp_{i,j}=\min\limits dp_{k,j-1}+cost (k+1,i)\),\(cost\) 很显然满足四边形不等式,于是直接分治优化,由于费用无法直接算出,所以需要使用“技巧”。

#include<bits/stdc++.h>
#define int long long
using namespace std;
bool Mbe;
constexpr int N=100003,M=400003,inf=1ll<<60,mod=998244353;
char I[22000038],*J=I;
inline int read()
{
	int x=0,y=1;
	while(*J<48||57<*J){if(*J=='-') y=-1;J++;}
	while(47<*J&&*J<58) x=(x<<1)+(x<<3)+(*J++^48);
	return x*y;
}

int n,K,a[N];
int buc[N],js,f[N],g[N];
int L,R;
inline void ins(int pos){js+=buc[a[pos]],buc[a[pos]]++;}
inline void del(int pos){--buc[a[pos]],js-=buc[a[pos]];}
inline void calc(int l,int r)
{
	while(L<l) del(L++);
	while(L>l) ins(--L);
	while(R<r) ins(++R);
	while(R>r) del(R--);
}
inline void solve(int l,int r,int pl,int pr) //f->g
{
	if(l>r) return;
	int mid=l+r>>1,nr=min(pr,mid-1),p=-1;
	g[mid]=inf;
	for(int i=pl,v;i<=nr;i++) 
	{
		calc(i+1,mid);
		if((v=f[i]+js)<g[mid]) p=i,g[mid]=v;
	}
	solve(l,mid-1,pl,p),solve(mid+1,r,p,pr);
}

bool Med;
signed main()
{
	fprintf(stderr,"%.4lf MB\n",(&Mbe-&Med)/1048576.0);
	#ifdef Marsrayd
	 	freopen("data.in","r",stdin);
	 	freopen("zj.out","w",stdout);
	#endif
	fread(I,1,22000038,stdin);
	n=read(),K=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=n;i++) f[i]=f[i-1]+buc[a[i]],js+=buc[a[i]],buc[a[i]]++;
	L=1,R=n;
	for(int i=1;i<K;i++)
	{
		solve(1,n,0,n-1);
		swap(f,g);
	}
	printf("%lld\n",f[n]);
	return 0;
}
例题 \(2\):\(\text{P5574}\)

给定 \(n,k\) 和长度为 \(n\) 的排列 \(a\),你要将 \(a\) 划分成 \(k\) 段,最小化每一段的顺序对个数的和。

\(\text{data range}\):\(n\le 25000,k\le 25\)。

\(dp_{i,j}\) 表示考虑到 \(a_i\) 分成了 \(j\) 个段,最小费用,然后直接树状数组 \(+\) 分治优化。

#include<bits/stdc++.h>
#define int long long
using namespace std;
bool Mbe;
constexpr int N=100003,M=400003,inf=1ll<<60,mod=998244353;
char I[22000038],*J=I;
inline int read()
{
	int x=0,y=1;
	while(*J<48||57<*J){if(*J=='-') y=-1;J++;}
	while(47<*J&&*J<58) x=(x<<1)+(x<<3)+(*J++^48);
	return x*y;
}

int n,K,a[N];
int tr[N],js,f[N],g[N];
int L,R;
inline int lowbit(int x){return x&-x;}
inline void upd(int x,int k){while(x<=n) tr[x]+=k,x+=lowbit(x);}
inline int ask(int x){int res=0;while(x) res+=tr[x],x-=lowbit(x);return res;}
inline void calc(int l,int r)
{
	while(L<l) upd(a[L],-1),js-=ask(a[L]-1),L++;
	while(L>l) --L,js+=ask(a[L]-1),upd(a[L],1);
	while(R<r) ++R,js+=R-L-ask(a[R]),upd(a[R],1);
	while(R>r) upd(a[R],-1),js-=R-L-ask(a[R]),R--;
}
inline void solve(int l,int r,int pl,int pr) //f->g
{
	if(l>r) return;
	int mid=l+r>>1,nr=min(pr,mid-1),p=-1;g[mid]=inf;
	for(int i=pl,v;i<=nr;i++) 
	{
		calc(i+1,mid);
		if((v=f[i]+js)<g[mid]) p=i,g[mid]=v;
	}
	solve(l,mid-1,pl,p),solve(mid+1,r,p,pr);
}

bool Med;
signed main()
{
	fprintf(stderr,"%.4lf MB\n",(&Mbe-&Med)/1048576.0);
	#ifdef Marsrayd
	 	freopen("data.in","r",stdin);
	 	freopen("zj.out","w",stdout);
	#endif
	fread(I,1,22000038,stdin);
	n=read(),K=read();
	for(int i=1;i<=n;i++) a[i]=read();
	reverse(a+1,a+n+1); //由于一开始看成逆序对了,这里 reverse 一下把顺序对变为逆序对。
	for(int i=1,v;i<=n;i++) js+=(v=i-1-ask(a[i])),f[i]=f[i-1]+v,upd(a[i],1);
	L=1,R=n;
	for(int i=1;i<K;i++)
	{
		solve(1,n,0,n-1);
		swap(f,g);
	}
	printf("%lld\n",f[n]);
	return 0;
}
例题 \(3\):\(\text{CF1603D}\)

斜率优化:1D/1D

算法简介

一类转移可以写成:\(f_i=\min/\max f_j+ cost _j+cost_i+F_iF_j\)(一些只与 \(i\) 有关的项,一些只与 \(j\) 有关的项,一与 \(i,j\) 都有关的项)。

这样的转移一般可以斜优(将关于 \(j\) 的变成 \(x,y\),关于 \(i\) 的变成 \(k,b\))。

具体地,将方程改写为 \(f_j+cost_j=F_iF_j+f_i-cost_i\) 设 \(y=f_j+cost_j,k=F_i,b=f_i-cost_i\)),那么它就是一个一次函数表达式 \(y=kx+b\),注意到 \(k\) 固定,所有的 \((x,y)\) 已知,由于 \(f_i=b+cost_i\)。所以要让 \(b\) 最大\(/\)小。

不妨设要求 $\min $,也就是说,给了你一堆点,斜率也固定,你要让 \(b\) 尽量小,所以维护一个下凸壳,让直线去切它就行。

  • 如果满足插入的点 \(x\) 有序,\(k\) 有序可以单调队列/单调栈维护。
  • 如果插入的只有 \(x\) 有序,需要在凸包上二分。
  • 否则需要:\(\text{set}\) 维护动态凸包 \(/\) 李超树。

坑:

  • 特判斜率不存在,记为 \(+\infty\) 或 \(-\infty\)。
例题 \(1\):\(\text{P3195}\)

顺序排列着 \(n\) 条线段,每条长度为 \(C_i\),你需要将这些线段划分成几组,一段放入一个容器,如果这一段包含从 \(i\) 到 \(j\) 的线段,容器长度为 \(x=j-i+\sum\limits_{k=i}^j C_k\),费用为 \(w=(x-L)^2\),其中 \(L\) 是给定常量,求最小的总费用可以装下所有线段。

\(\texttt{data range}\):\(1\le n\le 5\times 10^4\)。

记 \(f_i\) 表示将前 \(i\) 个玩具装箱的最小费用,\(s_i=\sum\limits_{k=1}^i C_k+1\)。

\(f_i=\min f_j+(s_i-s_j+L)^2\)。

化简:\(\underline{f_j+s_j^2}_{\ y}=\underline{2(s_i-L)}_{\ k}\times\underline{s_j}_{\ x}+\underline{f_i-(s_i-L)^2}_{\ b}\)。

\(x\) 单调,\(k\) 单调,单调队列即可。

#include<bits/stdc++.h>
typedef long long LL;
typedef long double LD;
using namespace std;
bool Mbe;
constexpr int N=200003,M=400003,inf=0x3f3f3f3f,mod=998244353;
constexpr LD eps=1e-10;
char I[22000038],*J=I;
inline int read()
{
	int x=0,y=1;
	while(*J<48||57<*J){if(*J=='-') y=-1;J++;}
	while(47<*J&&*J<58) x=(x<<1)+(x<<3)+(*J++^48);
	return x*y;
}

int n,hh,tt,q[N];
LL L,s[N],dp[N];
inline short dcmp(double A,double B){if(fabs(A-B)<eps) return 0;return A<B?-1:1;}
LL sq(LL x){return x*x;}
LL X(int i){return s[i];}
LL Y(int i){return dp[i]+sq(s[i]);}
LL slope(int i,int j){return (LD)(Y(i)-Y(j))/(X(i)-X(j));}

bool Med;
signed main()
{
	fprintf(stderr,"%.4lf MB\n",(&Mbe-&Med)/1048576.0);
	#ifdef Marsrayd
	 	freopen("data.in","r",stdin);
	 	freopen("zj.out","w",stdout);
	#endif
	fread(I,1,22000038,stdin);
	n=read(),L=read()+1;
	for(int i=1;i<=n;i++) s[i]=read()+s[i-1]+1;
	q[hh=tt=1]=0;
	for(int i=1;i<=n;i++)
	{
		while(hh<tt&&dcmp(slope(q[hh],q[hh+1]),2*(s[i]-L))<0) ++hh;
		int j=q[hh];dp[i]=dp[j]+sq(s[i]-s[j]-L);
		while(hh<tt&&dcmp(slope(q[tt-1],q[tt]),slope(q[tt],i))>0) --tt;
		q[++tt]=i;
	}
	printf("%lld\n",dp[n]);
	return 0;
}
例题 \(2\):\(\text{P5785}\)

\(n\) 个任务,每个有 \(t\) 和 \(c\) 两种属性,将 \(n\) 个任务划分为任意组,一组一组处理,处理一组的时候会产生额外的 \(s\) 时间(给定常量),一组的费用为结束时刻 \(\times\) \(\sum c\),求最小总费用。

\(\texttt{data range}\):\(1 \le n \le 3 \times 10^5\),\(1 \le s \le 2^8\),$ \left| T_i \right| \le 2^8$,\(0 \le C_i \le 2^8\)。

首先一个显然的状态是 \(f_{i,j}\) 表示前 \(i\) 个任务被分成了 \(j\) 组,最小总费用,为什么要记 \(j\) 呢,因为我们不知道前面有多少个 \(s\),于是可以考虑,贡献先算技巧,在新划分成一组的时候,将 \(s\) 产生的对于后面的影响一起算了,加入答案,这样再划分一组的时候就不需要考虑前面分为了几组了,于是记 \(f_i\) 表示前 \(i\) 个任务已完成,所有的 \(s\) 的总贡献已经计算的最小总费用。

转移:记 \(d_i=\sum\limits_{j=1}^i t_j,g_i=\sum\limits_{j=1}^ic_j\),\(f_i=\min\{ f_j +(d_i-d_j+s)(g_n-g_j)\}\)。

同时关于 \(i,j\) 的仅有 \(d_ig_j\) 考虑斜率优化。

化简完变为:\(\underline{f_j+d_jg_j-d_jg_n-g_js}_{\ y}=\underline{d_i}_{\ k}\times\underline{g_j}_{\ x}+\underline{f_i-d_ig_n}_{\ b}\)。

#include<bits/stdc++.h>
using namespace std;
bool Mbe;
constexpr int N=5600003,M=400003,inf=0x3f3f3f3f,mod=998244353;
double eps=1e-9;
char I[22000038],*J=I;
inline int read()
{
	int x=0,y=1;
	while(*J<48||*J>57) if(*J++=='-') y=-1;
	while(*J>=48&&*J<=57) x=(x<<1)+(x<<3)+(*J++^48);
	return x*y;
}
int n,s;
int hh,tt,q[N];
double t[N],c[N];
inline short dcmp(double A,double B){return fabs(A-B)<eps?0:A<B?-1:1;}
double f[N],d[N],g[N];
inline double sq(double x){return x*x;}
inline double X(int i){return g[i];}
inline double Y(int i){return f[i]+d[i]*g[i]-d[i]*g[n]-g[i]*s;}
inline double slope(int i,int j){if(dcmp(X(i),X(j))==0) return Y(j)>=Y(j)?1e18:-1e18;return (Y(i)-Y(j))/(X(i)-X(j));}
inline bool check(int i,double k)
{
	return dcmp(slope(q[i],q[i+1]),k)>0;
}
inline int find(double k)
{
    if(hh==tt) return 1;
	int l=1,r=tt,mid,res=r;
	while(l<=r)
	{
		mid=l+r>>1;
		if(check(mid,k)) r=mid-1,res=mid;
		else l=mid+1;
	}
	return res;
}
bool Med;
signed main()
{
	fprintf(stderr,"%.4lf MB\n",(&Mbe-&Med)/1048576.0);
	#ifdef Marsrayd
	 	freopen("data.in","r",stdin);
	 	freopen("zj.out","w",stdout);
	#endif
	fread(I,1,22000038,stdin);
	n=read(),s=read();
	for(int i=1;i<=n;i++) t[i]=read(),c[i]=read();
	for(int i=1;i<=n;i++) d[i]=d[i-1]+t[i],g[i]=g[i-1]+c[i];
	//x 单调增,k 单调增。
	q[hh=tt=1]=0;
	for(int i=1,j;i<=n;i++)
	{
		j=q[find(d[i])],f[i]=f[j]+(d[i]-d[j]+s)*(g[n]-g[j]);
		while(hh<tt&&dcmp(slope(q[tt-1],q[tt]),slope(q[tt],i))>0) --tt;
		q[++tt]=i;
	}
	printf("%.0lf\n",f[n]); 
	return 0;
}
例题 \(3\):\(\text{}\)

wqs 二分

wqs 二分优化常见于 2D / 1D 动态规划。其中一维是 选取物品个数

对于限制某某某个数的题目可以想想 wqs 二分

算法详解

wqs 二分,全称王钦石二分,又称带权二分,斜率凸优化。常用决策单调性的一些 \(dp\) 优化方法一起使用。

该算法常见于限制选取物品个数的题目中,标志明显,所以看起来比较套路。设 \(f_i\) 表示最多(或最少、恰好)选取 \(i\) 个物品的答案,若 \(f\) 为凸函数可以 wqs 二分,凸函数的判断靠感觉

\(nk\) 不行就是凸的。

举个例子:\(n\) 个数,最多分成 \(m\) 段,求每段的和的平方和的最小值。

  • 暴力:\(n^2m\)。
  • 决策单调性:\(nm\log n\)。
  • 斜率优化:\(nm\)。

首先复杂度是肯定带 \(n\) 的,然而复杂度花在 \(m\) 上有点浪费,考虑在 \(m\) 上做手脚,记 \(dp_{i,j}\) 表示将 \(a_1\sim a_i\) 划分成 \(j\) 段的答案,记 \(f_i=dp_{n,i}\),感性猜测 \(f\) 是凸的。

例题 \(1\):

给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(K\) 条白色边的生成树。

题目保证有解。

限定了白色边的边数,可以考虑 wqs 二分。

想要多选白色边就让白色边权值变小,想要少选就让白色边权值变大,二分变化的幅度即可。

标签:le,cost,int,mid,Part,单调,优化,dp
From: https://www.cnblogs.com/Marsrayd/p/17008666.html

相关文章

  • DPU1.1S完全兼容FT1.1S是一颗USB2.0 HUB芯片
    DPU1.1S是一款高性能、低功耗4口高速USB2.0HUB控制器,上行端口兼容高速480MHz和全速12MHz两种模式,4个下行端口兼容高速480MHz、全速12MHz、低速1.5MHz三种模式。DP......
  • #2153. 「SCOI2005」互不侵犯(状压DP)
    #2153.「SCOI2005」互不侵犯解题思路令dp[i][j][k]表示第i行的状态为j时,共放置k个国王的方案数。状态j的二进制即表示该行的放置方式,例如j为3时,放置的方式为101,即从右......
  • EF Core中Partition by实现
    一、SQL语句实现Partitionby是SQLServer数据库中提供的分区函数,跟Groupby不同的是,Partitionby能够按照分区返回所有记录,而Groupby只能返回一条记录。举个例子,有如下......
  • 国产DP4398 兼容替代CS4398 24Bit 192KHz数模转换芯片
    CS4398是一款24Bit/192KHz规格的解码芯片,它具有120分贝以上的讯噪比和动态范围,采用一个高级专用多位Delta-Sigma调制器,并整合了失配噪声整形技术。今天来讲讲它的国产替代......
  • C++概念之explicit,static成员,友元,内部类,匿名对象,拷贝对象的编译器优化(7千字长文详解!)
    c++详解之explicit,static成员,友元,内部类,匿名对象,拷贝对象的编译器优化关于对象的隐性类型转换类型转换我们知道当我们的将一个内置类型的变量强制赋值个另一个内置类型......
  • 解决 Ubuntu apt-get Could not get lock varlibdpkglock-frontend
    解决Ubuntuapt-getCouldnotgetlock/var/lib/dpkg/lock-frontend.E:Couldnotgetlock/var/lib/dpkg/lock-frontend.Itisheldbyprocess1927(unattended-......
  • U3D开发性能优化笔记(待增加版本.x)
    AmirFasshihi优化方案:一、遇到麻烦时要调用“垃圾回收器”(GarbageCollector,无用单元收集程序,以下简称GC)由于具有C/C++游戏编程背景,我们并不习惯无用单元收集程序的特定......
  • 前端性能优化
    多使用内存,缓存或其他方法减少CPU计算量,减少网络加载耗时(适用于所有编程的性能优化---空间换时间) 让加载更快1.减少资源体积:压缩代码2.减少访问次数:合并代码。SSR服......
  • 在 Ubuntu 22.04 上部署 WordPress
    很简单的事情被搞得很复杂,踩了很多坑……以及莫名其妙的错误。本来碰壁了之后会一拖再拖。昨天新冠阳性难受了一整天,晚上退烧了,不想学数学就想搞搞这个,第二天早上就弄好了......
  • COCOS2DX 3.0 优化提升渲染速度 Auto-batching
    COCOS2DX3.0优化提升渲染速度Auto-batchingAutoCulling动态缩减功能下面就来仔细看看吧:整合好的渲染提速干货:简介在游戏的绘制渲染中,往往消耗很多资源和内存,当绘制......