学习笔记——动态规划


大坑系列

动态规划 = 推式子 + 初始化 + 各种优化

普通dp

1.背包dp

背包大致有几种:

<1> 0-1背包

P1048 采药

首先我们设第 \(i\) 个“物品”的重量 \(w(i)\),价值 \(v(i)\),背包的容纳量为 \(W\),很容易发现这个状态转移就是“拿与不拿,买与不买”的问题,设置 \(dp_{i,j}\) 表示前 \(i\) 个物品中代价为 \(j\) 时所能得到最大的价值,既然由买与不买转换而来就能够得到:

\[dp_{i,j}=\max(dp_{i-1,j},dp_{i-1,j-w(i)}+v(i)) \]

代码实现为:

int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		w[i]=read(),v[i]=read();
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(w[i]>j){
				dp[i][j]=dp[i-1][j];
			}
			else{
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
			}
		}
	}
	printf("%d\n",dp[n][m]);
	return 0;
}

要注意到如果当前代价不够取第 \(i\) 个物品也就是没有选择时,也需要更新 \(dp_{i,j}\),可以发现空间复杂度是不够优秀的,可以把第一维滚掉。就变成了:

\[dp_j=\max(dp_j,dp_{j-w(i)}+v(i)) \]

注意这里的第二维应当使用倒序,考虑这一数组实际是在第 \(i\) 次循环是把第 \(i-1\) 次的结果对应赋值,也就是只需要对 \([w(i),W]\) 部分进行判断,那么为了防止先更新 \(dp_{j-w(i)}\) 使 \(dp_j\) 的结果错误,选择从大到小遍历,也就是倒序。

int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		w[i]=read(),v[i]=read();
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}

<2> 完全背包

P1616 疯狂的采药

即每种物品个数无限的背包,自然可以在0-1背包的基础上加一层枚举:

\[dp_{i,j}=\max_{k=0}^{+\infty}\{dp_{i-1,j-w(i)\times k}+v(i)\} \]

时间复杂度 \(O(nmk)\) ,上文0-1背包中提到的覆盖问题,其实也可以理解成会出现多次选一种物品的情况,恰恰符合完全背包的要求,于是就有:

\[dp_j=\max(dp_j,dp_{j-w(i)}+v(i)) \]

只需要把倒序改为正序。

int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		w[i]=read(),v[i]=read();
	}
	for(int i=1;i<=n;i++){
		for(int j=w[i];j<=m;j++){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}

<3> 多重背包

P1776 宝物筛选

对于每个物品 \(i\) 增加一个数量限制 \(k(i)\)

根据完全背包的枚举式子就能得到:

\[dp_{i,j}=\max_{k=0}^{k(i)}\{dp_{i-1,j-w(i)\times k}+v(i)\} \]

当然这个式子的复杂度中含有一个 \(\sum k(i)\),如何优化呢?

二进制优化

原理上可以理解成如果把一个数 \(p\) 按照二进制也就是 \(1,2,4,8,16\) 的方式拆分成 \(q\) 个,一定能通过这 \(q\) 个数相加得到 \([0,p]\) 中任意一个数,既然如此不如把这个物品看作是二进制拆分后的 \(q\) 个物品,按照0-1背包处理。

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		int tmpv=read(),tmpw=read(),k=read();
		for(int j=1;j<=k;j<<=1){
			v[++cnt]=j*tmpv,w[cnt]=j*tmpw;
			k-=j;
		}
		if(k) v[++cnt]=k*tmpv,w[cnt]=k*tmpw;
	}
	for(int i=1;i<=cnt;i++){
		for(int j=m;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}

<4> 混合背包

P1833 樱花

将以上三种dp混合在一起

int main(){
	scanf("%d:%d%d:%d%d",&sh,&sm,&eh,&em,&n);
	m=eh*60+em-sh*60-sm;
	//printf("%d %d %d %d\n",sh,sm,eh,em);
	for(int i=1;i<=n;i++){
		int a=read(),b=read(),c=read();
		if(!c) c=1e6;
		for(int j=1;j<=c;j<<=1){
			w[++cnt]=a*j,v[cnt]=b*j;
			c-=j;
		}
		if(c) w[++cnt]=a*c,v[cnt]=b*c;
	}
	for(int i=1;i<=cnt;i++){
		for(int j=m;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}

<5> 二维费用背包

P1855 榨取kkksc03

有双重代价限制的背包dp,处理方式自然是加一层循环。

int main(){
	n=read(),w1[0]=read(),w2[0]=read();
	for(int i=1;i<=n;i++){
		w1[i]=read(),w2[i]=read();
	}
	for(int i=1;i<=n;i++){
		for(int j=w1[0];j>=w1[i];j--){
			for(int k=w2[0];k>=w2[i];k--){
				dp[j][k]=max(dp[j][k],dp[j-w1[i]][k-w2[i]]+1);
			}
		}
	}
	printf("%d\n",dp[w1[0]][w2[0]]);
	return 0;
}

<6> 分组背包

P1757 通天之分组背包

有冲突限制就用 \(vector\) 存一下分组,接着正常0-1背包。

int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		w[i]=read(),v[i]=read();
		int k=read();
		cnt=max(cnt,k);
		g[k].push_back(i);
	}
	for(int i=1;i<=cnt;i++){
		for(int j=m;j>=0;j--){
			for(int k=0;k=w[now]){
					dp[j]=max(dp[j],dp[j-w[now]]+v[now]);
				}
			}
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}

例题

1.新年趣事之打牌

首先我们的目标答案应该为总重量 \(sum\) 与当前重量 \(W\) 之差,用 \(dp(i)\) 表示方案数,递推式同0-1背包,在状态转移的同时用 \(addmark(i)\) 表示在原有牌的基础上加入第几张牌可以得到数量为 \(i\),最后判断方案数并递归输出需要的牌即可。

inline void print(int k){
	if(k){
		print(k-w[addmark[k]]);
		printf("%d",addmark[k]);
		if(k!=m) printf(" ");
	}
	return;
}
int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		w[i]=read();
		sum+=w[i];
	}
	m=sum-m;
	dp[0]=1;
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){
			dp[j]+=dp[j-w[i]];
			if(dp[j]>5){
				dp[j]=2;
			}
			if(!addmark[j]&&dp[j-w[i]]){
				addmark[j]=i;
			}
		}
	}
	if(!dp[m]) printf("0\n");
	else if(dp[m]>1) printf("-1\n");
	else print(m);
	return 0;
}

2.线性dp

只能说是非常不花里胡哨的dp。

例题

1.求最长上升子序列

\(dp_i\) 表示以 \(a_i\) 为结尾的最大上升子序列长,很明显转移方程是:

\[dp_i=\max_{j=1}^{i-1}(dp_j+1) [a_i>a_j] \]

输出子序列一样是存储倒数第一个下标然后回溯即可。

inline void print(int pos){
	if(pos==-1) return;
	print(path[pos]);
	printf("%d ",a[pos]);
}
string s;
int main(){
	getline(cin,s);
	for(int i=0;ia[j]&&dp[j]+1>dp[i]){
				dp[i]=dp[j]+1;
				path[i]=j;
			}
		}
		if(dp[i]>ans){
			ans=max(ans,dp[i]);
			last=i;
		}
	}
	printf("max=%d\n",ans);
	print(last);
	return 0;
}

2.P1091 NOIP2004 提高组 合唱队形

观察条件数列的形式:

\[t_i\dots>t_{k-1}>t_k(1\leq i\leq k) \]

实质是由 \(t_i\) 左侧的一个最长上升序列,右侧的一个最长下降序列组合的,那么分别求以 \(t_i\) 为首和尾的上升和下降序列长,最后求和比较大小,答案为:

\[ans=n-\max_{i=1}^n(dp1(i)+dp2(i)-1) \]

int main(){
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
	}
	for(int i=1;i<=n;i++){
		dp1[i]=1;
		for(int j=1;ja[j]){
				dp1[i]=max(dp1[i],dp1[j]+1);
			}
		}
	}
	for(int i=n;i>=1;i--){
		dp2[i]=1;
		for(int j=n;j>i;j--){
			if(a[i]>a[j]){
				dp2[i]=max(dp2[i],dp2[j]+1);
			}
		}
	}
	for(int i=1;i<=n;i++){
		ans=max(ans,dp1[i]+dp2[i]-1);
	}
	printf("%d\n",n-ans);
	return 0;
}

3.P1020 NOIP1999 普及组 导弹拦截

第一问求最长不上升子序列,十分好做。

第二问需要用到Dilworth定理,大体意思就是需要的不上升子序列数等于最长上升子序列长度,同理可做。

4.P2196 NOIP1996 提高组 挖地雷

邻接矩阵记录路径(实际是有向图),设置 \(dp_i\) 表示以 \(i\) 结束的最大值,按照回溯方式输出路径。

5.P2782 友好城市

将每对友好城市按北岸升序排序,再求南岸的最长不下将子序列长,需要更优的 \(O(nlogn)\) 算法

6.P1481 魔族密码

求最长前缀链,本质和最长上升子序列是一样的,至于判断是不是前缀,可以用 \(hash\) 也可以简单粗暴地使用 \(s.substr(pos,len)\),表示字符串 \(s[pos:pos+len-1]\) 的部分。

int main(){
	n=read();
	for(int i=1;i<=n;i++){
		cin>>s[i];
	}
	for(int i=1;i<=n;i++){
		dp[i]=1;
		for(int j=1;j

7.求最长公共子序列

对于数组 \(s1(i)\)\(s2(j)\),设置 \(dp(i,j)\) 表示截止 \(s1(i)\)\(s2(j)\) 的最长公共子序列长,容易发现 \(dp(i,j)\) 只能由 \(dp(i-1,j-1)\)\(dp(i-1,j)\)\(dp(i,j-1)\) 转移得来,所以就有:

\[dp(i,j)=\max(dp(i-1,j),dp(i,j-1)) \]

\[dp(i,j)=\max(dp(i,j),dp(i-1,j-1))[s1(i)=s2(j)] \]

而如果求最长连续公共子序列,\(dp(i,j)\) 就表示以 \(s1(i)\)\(s2(j)\) 结尾的最长连续公共子序列长,也就只有当 \(s1(i)=s2(j)\) 的时候才会转移。

//非连续
int main(){
	scanf("%s",s1+1);
	scanf("%s",s2+1);
	len1=strlen(s1+1),len2=strlen(s2+1);
	for(int i=1;i<=len1;i++){
		for(int j=1;j<=len2;j++){
			dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
			if(s1[i]==s2[j]){
				dp[i][j]=dp[i-1][j-1]+1;
			}
		}
	}
	printf("%d\n",dp[len1][len2]);
	return 0;
}
//连续
int main(){
	scanf("%s",s1+1);
	scanf("%s",s2+1);
	len1=strlen(s1+1),len2=strlen(s2+1);
	for(int i=1;i<=len1;i++){
		for(int j=1;j<=len2;j++){
			if(s1[i]==s2[j]){
				dp[i][j]=dp[i-1][j-1]+1;
			}
			ans=max(ans,dp[i][j]);
		}
	}
	printf("%d\n",ans);
	return 0;
}

8.P2904 River Crossing S

很明显需要用到前缀和优化,\(sum(i)=sum(i-1)+a(i)\),我们给他再加上本来需要的运载费用(注意返程是空船所以只需要 \(m\) 的代价),也就是 \(sum(i)=sum(i)+m\times 2\),然后用 \(dp(i)\) 表示运前 \(i\) 头牛的最小花费,双层循环转移即可。

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		dp[i]=maxxn;
		a[i]=read();
		sum[i]=sum[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		sum[i]+=2*m;
	}
	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++){
			dp[j]=min(dp[j],dp[j-i]+sum[i]);
		}
	}
	printf("%d\n",dp[n]-m);
	return 0;
}

9.P2285 HNOI2004 打鼹鼠

看似三维,实则一维。

可以发现如果想要打第 \(i\) 只和第 \(j\) 只鼹鼠就需要两者之间的曼哈顿距离(平行于 \(x\) 轴和 \(y\) 轴的距离和)小于等于出现的时间差,用 \(dis(i,j)\) 表示这个距离,\(t(i,j)\) 表示这个时间差,根据判断方式用 \(dp(i)\) 表示打到第 \(i\) 只鼹鼠时最多能打几只鼹鼠。易得:

\[dp(i)=\max_{j=1}^{i-1}x\{dp(j)+1\}[dis(i,j)<=t(i,j)] \]

int main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		a[i].t=read(),a[i].x=read(),a[i].y=read();
	}
	for(int i=1;i<=m;i++){
		dp[i]=1;
		for(int j=1;j=abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y)){
				dp[i]=max(dp[i],dp[j]+1);
			}
		}
		ans=max(ans,dp[i]);
	}
	printf("%d\n",ans);
	return 0;
}

所以就是一个距离比较特殊的图而已。

3.坐标dp

比较有区间dp的味儿了,基本上是考虑转移方向或者考虑建图。

例题

1.矩阵取数(简单版)

在数列中从队首或队尾取元素,第 \(i\) 次取第 \(k\) 个元素对答案的贡献为 \(a(k)\times 2^i\),求答案最大值。

我们用 \(dp(i,j)\) 表示取剩下区间 \([i,j]\) 时已经取过的答案最大值,不难发现 \(dp(i,i)=a(i)\times 2^n\),那么区间 \([i,j]\) 一定是由 \([i+1,j]\)\([i,j-1]\)\(i\)\(j\) 得来,如果 \(l=j-i\),就有转移方程:

\[dp(i,j)=\max(dp(i+1,j)+a(i)\times 2^{n-l},dp(i,j-1)+a(j)\times 2^{n-l}) \]

inline ll q_pow(ll x,ll p){
	ll ans=1;
	while(p){
		if(p&1){
			ans=ans*x;
		}
		p>>=1;
		x=x*x;
	}
	return ans;
}
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		dp[i][i]=a[i]*q_pow(2,n);
	}
	for(int l=1;l

然而进阶版需要写高精度……

//高精度
int n;
struct node{
	int num[505],len;
	inline void clear(){
		memset(num,0,sizeof(num));
		len=0;
	}
	inline void print(){
		for(int i=len;i>=1;i--){
			printf("%d",num[i]);
		}
		printf("\n");
	}
	node operator + (const node &tmp){
		node res;
		res.clear();
		res.len=max(len,tmp.len)+1;
		for(int i=1;i<=res.len;i++){
			res.num[i]+=num[i]+tmp.num[i];
			if(res.num[i]>9){
				res.num[i+1]+=res.num[i]/10;
				res.num[i]%=10;
			}
		}
		while(res.num[res.len]) res.len++;
		while(!res.num[res.len]&&res.len>1) res.len--;
		return res;
	}
	node operator * (const node &tmp){
		node res;
		res.clear();
		res.len=len+tmp.len;
		for(int i=1;i<=len;i++){
			for(int j=1;j<=tmp.len;j++){
				res.num[i+j-1]+=num[i]*tmp.num[j];
				if(res.num[i+j-1]>9){
					res.num[i+j]+=res.num[i+j-1]/10;
					res.num[i+j-1]%=10;
				}
			}
		}
		while(res.num[res.len]) res.len++;
		while(!res.num[res.len]&&res.len>1) res.len--;
		return res;
	}
}a[105],dp[105][105],power[105],base;
inline node Max(node p,node q){
	if(p.len>q.len) return p;
	else if(p.len=1;i--){
			if(p.num[i]>q.num[i]) return p;
			else if(p.num[i]

2.P1005 矩阵取数(恶心版)

因为每一行的答案实际是独立的,那么在原题的基础上套一层枚举每一行的循环,求和。

int n,m;
struct node{
	int num[505],len;
	inline void clear(){
		memset(num,0,sizeof(num));
		len=0;
	}
	inline void print(){
		for(int i=len;i>=1;i--){
			printf("%d",num[i]);
		}
		printf("\n");
	}
	node operator + (const node &tmp){
		node res;
		res.clear();
		res.len=max(len,tmp.len)+1;
		for(int i=1;i<=res.len;i++){
			res.num[i]+=num[i]+tmp.num[i];
			if(res.num[i]>9){
				res.num[i+1]+=res.num[i]/10;
				res.num[i]%=10;
			}
		}
		while(res.num[res.len]) res.len++;
		while(!res.num[res.len]&&res.len>1) res.len--;
		return res;
	}
	node operator * (const node &tmp){
		node res;
		res.clear();
		res.len=len+tmp.len;
		for(int i=1;i<=len;i++){
			for(int j=1;j<=tmp.len;j++){
				res.num[i+j-1]+=num[i]*tmp.num[j];
				if(res.num[i+j-1]>9){
					res.num[i+j]+=res.num[i+j-1]/10;
					res.num[i+j-1]%=10;
				}
			}
		}
		while(res.num[res.len]) res.len++;
		while(!res.num[res.len]&&res.len>1) res.len--;
		return res;
	}
}a[105][105],dp[105][105],power[105],base,ans;
inline node Max(node p,node q){
	if(p.len>q.len) return p;
	else if(p.len=1;i--){
			if(p.num[i]>q.num[i]) return p;
			else if(p.num[i]

3.P1006 NOIP2008 提高组 传纸条

很离谱的四维dp。

首先从 \((1,1)\)\((n,m)\) 再从 \((n,m)\)\((1,1)\) 不能重复的最大值和从 \((1,1)\)\((n,m)\) 走两次不重复的最大值是等价的,这样考虑更加简单。

设置 \(dp(i,j,k,l)\) 表示第一次走到 \((i,j)\),第二次走到 \((k,l)\),因为只能向下或向右,肯定由四种情况转移而来,就有:

\[dp(i,j,k,l)=\max(dp(i-1,j,k-1,l)dp(i-1,j,k,l-1),dp(i,j-1,k-1,l),dp(i,j-1,k,l-1))+a(i,j)+a(k,l) \]

注意这里因为不能路径重复所以要判断 \((i,j)\)\((k,l)\) 是否是同一个点。

玄学的讲 \(dp(n,m,n,m)\) 为最终答案。

inline int Max(int a1,int a2,int a3,int a4){
	return max(max(a1,a2),max(a3,a4));
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			a[i][j]=read();
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int k=n;k>=1;k--){
				for(int l=m;l>=1;l--){
					dp[i][j][k][l]=Max(dp[i-1][j][k-1][l],dp[i-1][j][k][l-1],dp[i][j-1][k-1][l],dp[i][j-1][k][l-1])+a[i][j];
					if(i!=k||j!=l) dp[i][j][k][l]+=a[k][l];
				}
			}
		}
	}
	printf("%d\n",dp[n][m-1][n-1][m]);
	return 0;
}

4.P1004 NOIP2000 提高组 方格取数

和传纸条是一样的……

5.P1854 花店橱窗问题

题意就是在 \(n\times m\) 的方格中每行取一个数,并且取出数的横纵编号都是单调递增的,求最大值。

不难想到设 \(dp(i,j)\) 表示第 \(i\) 行选在 \((i,j)\) 取,那么转移方程为:

\[dp(i,j)=\max_{k=i-1}^{j-1}\{dp(i-1,k)+a(i,j)\} \]

至于 \(k\) 的范围为什么是 \([i-1,j-1]\),原因是每一行都要选择一个位置,因此第 \(i-1\) 行起码要选在 \((i-1,i-1)\) 的位置。至于输出路径依然是在修改的时候用 \(nxt(i,j)\) 记录上一位置,回溯输出。

注意每层循环的范围,可以减少无实际意义的运算,例如 \(i\in [1,n],j\in [i,m-n+i],k\in [i-1,j-1]\)

inline void print(int ord,int pos){
	if(ord==1){
		printf("%d ",pos);
		return;
	}
	print(ord-1,nxt[ord][pos]);
	printf("%d ",pos);
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			a[i][j]=read();
			dp[i][j]=minxn;
		}
	}
	for(int i=1;i<=n;i++){
		dp[0][i]=0;
	}
	for(int i=1;i<=n;i++){
		for(int j=i;j<=m-n+i;j++){
			for(int k=i-1;kdp[i][j]){
					dp[i][j]=dp[i-1][k]+a[i][j];
					nxt[i][j]=k;
				}
			}
		}
	}
	for(int i=n;i<=m;i++){
		if(dp[n][i]>ans){
			ans=dp[n][i];
			last=i;
		}
	}
	printf("%d\n",ans);
	print(n,last);
	return 0;
}

6.P2690 Apple Catching G

其实和在 \(n\times 2\) 的0-1方格中取数没有什么区别,是否取到就看移动次数模 \(2\) 的结果。

int main(){
	t=read(),w=read();
	for(int i=1;i<=t;i++){
		a[i]=read();
	}
	for(int i=1;i<=t;i++){
		for(int j=0;j<=min(t,w);j++){
			if(!j){
				dp[i][j]=dp[i-1][j];
			}
			else{
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]);
			}
			if(a[i]==j%2+1){
				dp[i][j]++;
			}
		}
	}
	for(int i=0;i<=w;i++){
		ans=max(ans,dp[t][i]);
	}
	printf("%d\n",ans);
	return 0;
}

4.区间dp

相比于前面三种dp的状态转移是由前到后也就是考虑前 \(i\) 个的贡献,区间dp更像是从小到大的转移,多数的状态设置为 \(dp(i,j)\) 表示区间 \([i,j]\) ,用 \(k\) 将区间分成两段,即 \([i,k]\)\([k+1,j]\) 进行转移。

例题

1.石子合并 简单版 普通版 强化版

首先最基本的转移是

\[dp(i,j)=\max_{k=i}^{j-1}\{dp(i,k)+dp(k+1,j)\}+sum(i,j) \]

维护一个前缀和即可(求最小值同理),如果是一个环的话,自然是把 \([1,n]\) 复制一遍讨论。

简单版

int n;
int a[305],sum[305];
int dp1[305][305],dp2[305][305];
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		sum[i]=sum[i-1]+a[i];
	}
	for(int l=1;l

普通版

int main(){
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		a[i+n]=a[i]; 
	}
	for(int i=1;i<=2*n;i++){
		sum[i]=sum[i-1]+a[i];
	} 
	for(int l=1;l

强化版是基于 \(\text{GrasiaWachs}\) 算法的,一个感性的理解是 \(a_{i-1},a_i,a_{i+1}\) 三堆石子,第二次合并的代价一定为 \(a_{i-1}+a_i+a_{i+1}\),而第一次合并一定与 \(a_i\) 有关,所以只需要比较 \(a_{i-1}\)\(a_{i+1}\) 的大小关系就可以得到合并顺序。

3.P1018 NOIP2000 提高组 乘积最大

维护一个 \(sum(i,j)\) 表示 \(s[i:j]\) 部分的数,用 \(dp(i,j)\) 表示 \(s[1:i]\) 分成 \(j\) 各部分的最大乘积,不难得到:

\[dp(i,j)=\max_{k=1}^{i}\{dp(k,j-1)\times sum(k+1,j)\} \]

如果输出路径就和线性差别不大了。

4.P1063 NOIP2006 提高组 能量项链

首先看到形成一个环,自然要断 \(n\) 的环成 \(n\times 2\) 的链,然后按照经典区间dp的三层循环——区间长度->区间起点->区间断点,最后答案为长度为 \(n\) 区间的最大值。

int main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
        a[n+i]=a[i];
    }
    for(int l=1;l<=n;l++){
        for(int i=1,j=i+l;i<=2*n&&j<=2*n;i++,j++){
            for(int k=i+1;k<=j-1;k++){
                dp[i][j]=max(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]);
            }
        }
    }
    ll ans=0;
    for(int i=1;i<=n;i++){
       ans=max(ans,dp[i][i+n]);
    }
    printf("%lld",ans);
}

5.P4342 IOI1998 Polygon

依旧是断环为链,状态设置也如出一辙,用 \(f\) 表示最大值,加法运算自然是 \(f(i,j)=\max_{k=i}^{j-1}\{f(i,k)+f(k+1,j)\}\),可问题出在乘法上,如果出现两个绝对值较大的负数,会存在运算的实际最大值不等于两个子区间乘积,因此还要维护最小值 \(g\)

无脑的转移是模拟四种搭配:

\[f(i,j)=\max_{k=i}^{j-1}\{max(f(i,k)\times(f(k+1,j),f(i,k)\times g(k+1,j),g(i,k)\times f(k+1,j),g(i,k)\times g(k+1,j))\} \]

\[g(i,j)=\min_{k=i}^{j-1}\{min(f(i,k)\times(f(k+1,j),f(i,k)\times g(k+1,j),g(i,k)\times f(k+1,j),g(i,k)\times g(k+1,j))\} \]

第二问输出断点只需要遍历答案符合的 \([i,i+n-1]\)

6.P2890 Cheapest Palindrome G

可以发现把一个字符串修改成回文串,删除多余的字符与增加缺失的字符其实是等价的,且对后续的答案没有影响,那么实际上修改的代价就是增加和修改的最小值。

map a;
int main(){
    n=read(),m=read();
    scanf("%s",s+1);
    for(int i=1;i<=n;i++){
        char ch;
        cin>>ch;
        int num1=read(),num2=read();
        a[ch]=min(num1,num2);
        //printf("%d\n",a[ch]);
    }
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=m;i++){
        dp[i][i]=0;
    }
    for(int l=1;l

5.树形dp

现在作为背景的结构转移到了树上,区别在于,之前的线性dp从前到后或从后到前遍历,区间dp从小区间到大区间遍历,而树形dp是从叶子结点到根结点遍历,形式类似于深搜。

例题

1.P1040 加分二叉树

其实是区间dp,因为已知中序遍历所以 \([l,r]\) 的根一定在该范围内,记录每一段的根结点按照先序遍历输出即可。

2.P2015 二叉苹果树

把以 \(u\) 根的子树分成两部分,然后遍历在当前判断的子树中保留几根树枝,取最大值即可。

3.P2014 选课

同上。

inline void add_edge(int u,int v){
    e[++cnt].to=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
inline void f(int u,int fa){
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        f(v,u);
        siz[u]+=siz[v]+1;
        for(int j=min(m,siz[u]);j>=1;j--){
            for(int k=min(j-1,siz[v]);k>=0;k--){
                dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[v][k]+w[v]);
            }
        }
    }
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++){
        int u=read();
        w[i]=read();
        add_edge(u,i);
        add_edge(i,u);
    }
    f(0,0);
    printf("%d\n",dp[0][m]);
    return 0;
}

4.P1352 没有上司的舞会

状态的设置关乎参加不参加,也就是 \(dp(u,0/1)\) 表示以 \(u\) 为根的子树中根结点对答案是否产生贡献是答案的最大值,设集合 \(G\) 表示节点 \(u\) 的子节点集,由题意可以得到:

\[dp(u,0)=\max_{v\in G}\{dp(v,0),dp(v,1)\} \]

\[dp(u,1)=\max_{v\in G}\{dp(v,0)\}+w(u) \]

输出 \(\max(dp(rt,0),dp(rt,1))\) 即可。

inline void add_edge(int u,int v){
    e[++cnt].to=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
inline void f(int u,int fa){
    dp[u][0]=0,dp[u][1]=w[u];
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        f(v,u);
        dp[u][0]+=max(dp[v][0],dp[v][1]);
        dp[u][1]+=dp[v][0];
    }
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        w[i]=read();
    }
    for(int i=1;i

5.小胖守皇宫

同上一题,本题的状态设置分为三种 \(dp(u,0/1/2)\) 分别表示如下三种情况

  • 状态 \(0\) 表示该节点被自己看守。
  • 状态 \(1\) 表示该节点被子节点看守。
  • 状态 \(2\) 表示该节点被父节点看守,也就是遍历到该子树时没有被看守。

状态 \(0\) 和状态 \(2\) 的转移是相对好转移的,因为当节点被自己看守时,子节点的状态没有影响,因此有:

\[dp(u,0)=\min_{v\in G}\{dp(v,0),dp(v,1),dp(v,2)\} \]

而当节点被父节点看守时,若保证子节点被看守,则子节点的状态只能是已经被看守,因此又有:

\[dp(u,2)=\min_{v\in G}\{dp(v,0),dp(v,1)\} \]

剩下的状态 \(1\) 是相对复杂的,因为要保证所有的子节点中至少存在一个是由状态 \(0\) 转移来的,除该条件外,实质上与状态 \(2\) 是没有区别的,因此转移可以建立在状态 \(2\) 答案的基础上进行。

容易发现,当且仅当对于任意一个 \(v\in G\) 都满足 \(dp(v,0)>dp(v,1)\) 时才会与条件矛盾,因此只需要遍历对每一个 \(v\) 都进行处理,找出在 \(dp(v,0)\) 并不优于 \(dp(v,1)\) 时二者差的最小,与 \(dp(u,2)\) 相加,即:

\[dp(u,1)=\min_{v\in G}\{dp(u,2)-min(dp(v,0),dp(v,1))+dp(v,0)\} \]

如果取 \(dp(v,0)\) 时自然是对答案没有影响的,因此状态转移正确。

inline void add_edge(int u,int v){
    e[++cnt].to=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
int dp[maxn][3];//0自己,1儿子,2父亲
int rt;
inline void f(int u,int fa){
    dp[u][0]=w[u],dp[u][1]=maxxn,dp[u][2]=0;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        f(v,u);
        dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
        dp[u][2]+=min(dp[v][0],dp[v][1]);
    }
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        dp[u][1]=min(dp[u][1],dp[u][2]-min(dp[v][0],dp[v][1])+dp[v][0]);
    }
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        int u=read();
        w[u]=read();
        int siz=read();
        for(int j=1;j<=siz;j++){
            int v=read();
            add_edge(u,v);
            add_edge(v,u);
            vis[v]=1;
        }
    }
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            rt=i;
            break;
        }
    }
    f(rt,0);
    printf("%d\n",min(dp[rt][0],dp[rt][1]));
    return 0;
}

6.P2585 ZJOI2006 三色二叉树

只需要按照先序遍历建树,再自上而下更新即可,用 \(0/1/2\) 表示三种颜色,其中一种的状态有另外两种转移而来。

二次扫描和换根dp

根不确定的树形dp问题。

首先一个比较直接的解法是 \(O(n^2)\) 的,把每个节点当作根结点都跑一次dp,然而复杂度极其不优秀。

经典思路:对根结点为 \(1\) 时的做一次dp,然后二次扫描考虑改变根节点后对答案的影响,并更新每一个答案。

7.P3478 POI2008 STA-Station/P2986 USACO10MAR Great Cow Gathering G

类似的两道板子题。

首先以 \(1\) 为根节点的遍历出 \(dep(u),siz(u),dp(1)\) 非常好做,这里不加赘述,主要说说二次扫描。

(为了美观选择以 \(5\) 为初始的根节点)

考虑已知 \(dp(5)\) 转移 \(dp(4)\),以 \(4\) 为根的子树深度会集体 \(-1\),而除此以外的子树深度会集体 \(+1\),因此就有:

\[dp(4)=dp(5)-siz(4)+(n-siz(4))=dp(5)+n-siz(4)\times \]

推广到一般就是:

\[dp(v)=dp(u)+n-siz(v)\times 2 \]

注意二次扫描时递归与更新的顺序

inline void dfs(int u,int fa){
    dep[u]=dep[fa]+1,siz[u]=1;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        dfs(v,u);
        siz[u]+=siz[v];
    }
}
ll dp[maxn];
inline void secdfs(int u,int fa){
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        dp[v]=dp[u]+n-2*siz[v];
        secdfs(v,u);
    }
}
ll ans=-1,pos;
int main(){
    n=read();
    for(int i=1;ians){
            ans=dp[i];
            pos=i;
        }
    }
    printf("%lld\n",pos);
    return 0;
}

而对于第二题,唯一的区别是 \(siz(u)\) 表示的是奶牛个数,还需要维护 \(wsiz(u)\) 表示代价,而二次扫描时的转移方程有一点点变化:

\[dp(v)=dp(u)+(siz(1)-siz(v)\times 2)\times w \]

此时奶牛总数为 \(siz(1)\),而对答案贡献与权值有关。

开longlong

8.P3047 Nearby Cows G

\(dp(u,i)\) 表示距离节点 \(u\) 的距离为 \(j\) 的节点权值和,那么第一次得出的答案就是 \(\sum_{i=0}^{m}dp(1,i)\),接下来考虑二次扫描,可以发现第一次更新时是子节点 \(v\) 对父节点 \(u\) 贡献,而二次扫描是父亲节点 \(u\) 对子节点 \(v\) 贡献,因此要先删去原先 \(v\) 的贡献再修改 \(v\),注意回溯以及顺序。

换根dp越来越像莫队了。

inline void dfs(int u,int fa){
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        dfs(v,u);
        for(int j=1;j<=m;j++){
            dp[u][j]+=dp[v][j-1];
        }
    }
}
inline void secdfs(int u,int fa){
    for(int i=0;i<=m;i++){
        ans[u]+=dp[u][i];
    }
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        for(int j=1;j<=m;j++){
            dp[u][j]-=dp[v][j-1];
        }
        for(int j=1;j<=m;j++){
            dp[v][j]+=dp[u][j-1];
        }
        secdfs(v,u);
        for(int j=1;j<=m;j++){
            dp[v][j]-=dp[u][j-1];
        }
        for(int j=1;j<=m;j++){
            dp[u][j]+=dp[v][j-1];
        }
    }
}
int main(){
    n=read(),m=read();
    for(int i=1;i

9.CF708C Centroids

题解

6.数位dp

待更

7.状压dp

核心是在 \(n\) 并不多时,把每一种状态 \(0-1\) 转移成十进制的数。

例题

1.P1896 SCOI2005 互不侵犯

预处理出对于单行而言合法的情况,再在转移时判断是否有相邻。

int n,m;
int sit[2005],sitcnt[2005],cnt;
inline void dfs(int sitk,int cntk,int pos){
    if(pos>=n){
        sit[++cnt]=sitk,sitcnt[cnt]=cntk;
        return;
    }
    dfs(sitk,cntk,pos+1);
    dfs(sitk|(1<>1)) continue;
                for(int l=m;l>=sitcnt[j];l--){
                    dp[i][j][l]+=dp[i-1][k][l-sitcnt[j]];
                }
            }
        }
    }
    for(int i=1;i<=cnt;i++){
        ans+=dp[n][i][m];
    }
    printf("%lld\n",ans);
    return 0;
}

2.P2704 NOI2001 炮兵阵地

思路同上,需要将地形也压成状态

3.P2831 愤怒的小鸟

考虑把目前打掉的猪压成一个状态,把过任意两点的抛物线能打掉的猪压成一个状态。

inline void get_ab(db &A,db &B,db a1,db a2,db b1,db b2,db c1,db c2){
    B=(a1*c2-a2*c1)/(a1*b2-a2*b1);
    A=(c1-b1*B)/a1;
}
int main(){
    t=read();
    for(int i=0;i<(1<<18);i++){
        int j;
        for(j=1;j<=18 && i&(1<<(j-1));j++);
        unbit[i]=j;
    }
    while(t--){
        memset(dp,0x3f,sizeof(dp));
        memset(para,0,sizeof(para));
        n=read(),m=read();
        for(int i=1;i<=n;i++){
            scanf("%lf%lf",&x[i],&y[i]);
        }
        dp[0]=0;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(fabs(x[i]-x[j])-eps) continue;
                for(int k=1;k<=n;k++){
                    if(fabs(a*x[k]*x[k]+b*x[k]-y[k])

4.P3622 APIO2007 动物园

维护出每个区间是否保留动物状态的贡献,之后枚举状态取最大值即可。

int main(){
    n=read(),m=read();
    for(int i=1;i<=m;i++){
        int e=read(),f=read(),l=read();
        int sitf=0,sitl=0;
        for(int j=1;j<=f;j++){
            int x=read();
            x=(x-e+n)%n;
            sitf|=(1<

优化dp

1.单调队列优化dp

单调队列优化=提出与内层循环无关的数据,把剩余有关最大值最小值的问题用单调队列维护,要求转移范围是滑动的。

例题

1.CF372C Watching Fireworks is Fun

\(dp(i,j)\) 表示放第 \(i\) 个烟花时,在位置 \(j\) 所得到的开心值的最大值。

我们设 \(dx=d\times (t_i-t_{i-1})\),也就代表可以移动到 \(j\) 的最大距离,不难得出转移方程:

\[dp(i,j)=\max_{k=j-dx}^{j+dx} \{dp(i-1,k)+b_i-|a_i-j|\} \]

因为 \(b_i\) 累加的结果不变,所以可以在转移方程中删去,在结果中加上 \(sum_b\),也就化简有:

\[dp(i,j)=\max_{k=j-dx}^{j=dx} \{dp(i-1,k)\}-|a_i-j| \]

这里 \(-|a_i-j|\) 的结果与 \(k\) 是无关的,故可以提出。而这个值实际为负,可以维护其绝对值的最小值。不难发现,我们每个值,都是从 \([j-dx,j+dx]\) 之间的最值得到的,考虑单调队列优化。

只需要做如下两种操作:

  • 队首元素是当前最优且最前,如果超出范围需要弹出
  • 队尾元素是可能最优且最后,如果没有新入队的优需要弹出

但是我们的单调队列只能支持到 \(j\) 的范围,所以把这个区间拆分开,正反各跑一遍。

int main(){
    n=read(),m=read(),d=read();
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=m;i++){
        a[i]=read(),b[i]=read(),t[i]=read();
        sum+=b[i];
    }
    for(int i=1;i<=n;i++){
        dp[1][i]=Abs(a[1]-i);
    }
    for(int i=2;i<=m;i++){
        ll dx=(t[i]-t[i-1])*d;
        memset(dp[i&1],0x3f,sizeof(dp[i&1]));
        head=1,tail=0;
        for(int j=1;j<=n;j++){
            while(head<=tail&&q[head]dp[i&1^1][j]) tail--;
            q[++tail]=j;
            dp[i&1][j]=Min(dp[i&1][j],dp[i&1^1][q[head]]+Abs(a[i]-j));
        }
        head=1,tail=0;
        for(int j=n;j>=1;j--){
            while(head<=tail&&q[head]>j+dx) head++;
            while(head<=tail&&dp[i&1^1][q[tail]]>dp[i&1^1][j]) tail--;
            q[++tail]=j;
            dp[i&1][j]=Min(dp[i&1][j],dp[i&1^1][q[head]]+Abs(a[i]-j));
        }
    }
    ans=0x3f3f3f3f3f3f3f3f;
    for(int i=1;i<=n;i++){
        ans=Min(ans,dp[m&1][i]);
    }
    printf("%lld\n",sum-ans);
    return 0;
} 

2.P2569 SCOI2010 股票交易

自然是 \(dp(i,j)\) 表示第 \(i\) 天持有 \(j\) 只股票的最高收益,不难发现有,不操作、买与卖三种状态。

不操作的转移十分朴素,\(dp(i,j)=\max(dp(i,j),dp(i-1,j))\)

其实买卖的操作无需枚举 \(i-w-1\) 及以前的天数,因为不操作的情况已经转移到了后一天,因此从 \(dp(i-w-1,k)\) 转移即可,就能得到两个转移方程。

买:

\[dp(i,j)=\max_{k=j-as_i}^{j-1}\{dp(i-w-1,k)-ap_i\times (j-k)\} \]

提出与 \(k\) 的项无关得到:

\[dp(i,j)=\max_{k=j-as_i}^{j-1}\{dp(i-w-1,k)+ap_i\times k\}-ap_i\times j \]

卖:

\[dp(i,j)=\max_{j+1}^{j+bs_i}\{dp(i-w-1,k)+bp_i\times(k-j)\} \]

同理化简,可以得到:

\[dp(i,j)=\max_{j+1}^{j+bs_i}\{dp(i-w-1,k)+bp_i\times k\}-bp_i\times j \]

明显括号里面的是可以用单调队列优化的。

int main(){
    n=read(),m=read(),w=read();
    for(int i=1;i<=n;i++){
        ap[i]=read(),bp[i]=read(),as[i]=read(),bs[i]=read();
    }
    memset(dp,~0x3f,sizeof(dp));
    for(int i=0;i<=n;i++){
        dp[i][0]=0;
    }
    for(int i=1;i<=n;i++){
        for(int j=0;j<=as[i];j++){
            dp[i][j]=-1*j*ap[i];
        }
        for(int j=0;j<=m;j++){
            dp[i][j]=max(dp[i][j],dp[i-1][j]);
        }
        if(i>=w+1){
            head=1,tail=0;
            for(int j=0;j<=m;j++){
                while(head<=tail&&q[head]=dp[i-w-1][q[tail]]+ap[i]*q[tail]) tail--;
                q[++tail]=j;
                if(head<=tail) dp[i][j]=max(dp[i][j],dp[i-w-1][q[head]]-ap[i]*(j-q[head]));
            }
            head=1,tail=0;
            for(int j=m;j>=0;j--){
                while(head<=tail&&q[head]>j+bs[i]) head++;
                while(head<=tail&&dp[i-w-1][j]+bp[i]*j>=dp[i-w-1][q[tail]]+bp[i]*q[tail]) tail--;
                q[++tail]=j;
                if(head<=tail) dp[i][j]=max(dp[i][j],dp[i-w-1][q[head]]+bp[i]*(q[head]-j));
            }
        }
    }
    int ans=-1;
    for(int i=0;i<=m;i++){
        ans=max(ans,dp[n][i]);
    }
    printf("%d\n",ans);
    return 0;
}

3.P2254 NOI2005 瑰丽华尔兹

忽略起始位置等等,每个区间都在整个地图中每行或每列用单调队列d优化p,滚动数组减少空间。

4.P2627 Mowing the Lawn G

\[dp_i=\max_{k=i-k}^{i}\{dp_{j-1}+sum_i-sum_j\} \]

\[dp_i=\max_{k=i-k}^{i}\{dp_{j-1}-sum_j\}+sum_i \]

常规单调队列优化即可。

2.斜率优化dp

讲真很难的优化方式。

简单说一下思想:当递推式中出现了形如 \(a[i]\times b[j]+c[i]+d[j]\) 的式子时,显然单调队列优化是不可行的,因此我们把其化作一次函数 \(b=y-kx\) 的形式,就能得到一个凸包,用单调队列去维护凸包。

例题

1.P3195 HNOI2008 玩具装箱

先求前缀和 \(pre\),直接可以得到的递推式:\(dp_i=\min_{j=1}^{i-1}\{dp_j+(pre_i-pre_j+i-j-1-l)^2\}\)

\(sum_i=pre_i+1,l'=l+1\),为方便观察去掉 \(\min\),就能得到 \(dp_i=dp_j+(sum_i-sum_l-l')^2\),也就有:\(dp_i=dp_j+sum_i^2+(sum_j+l')^2-2sum_isum_j-2sum_il'\),之后移项成上述式子就有 \(dp_i-sum_i^2+2sum_il'=dp_j+(sum_j+l')^2-2sum_i\times sum_j\),其中 \(b=dp_i-sum_i^2+2sum_il',y=dp_j+(sum_j+l')^2,k=2sum_i,x=sum_j\)

之后我们得到了一个下凸包,让斜率在保证大于 \(k\) 的情况下尽量小,单调队列维护这个凸包。

ll n,l;
ll sum[maxn],dp[maxn];
ll q[maxn],head,tail;
inline ll X(ll i){
    return sum[i];
}
inline ll Y(ll i){
    return dp[i]+(sum[i]+l)*(sum[i]+l);
}
inline ldb slope(ll i,ll j){
    return (ldb)(Y(j)-Y(i))/(X(j)-X(i));
}
int main(){
    n=read(),l=read()+1;
    for(int i=1;i<=n;i++){
        sum[i]=read()+sum[i-1]+1;
    }
    head=1,tail=1;
    q[1]=0;
    for(int i=1;i<=n;i++){
        while(head=slope(q[tail-1],i)) tail--;
        q[++tail]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}

2.P3628 APIO2010 特别行动队

借本题谈一谈斜率优化的常规做法。

首先还是维护前缀和 \(sum\),正常的递推式(省略 \(\min\))是 \(dp_i=dp_j+a(sum_i-sum_j)+b(sum_i-sum_j)+c\),我们把式子打开,只 \(i\) 相关的移到等式左边,就能得到 \(dp_i=dp_j+asum_i^2-2asum_isum_j+asum_j^2+bsum_i-bsum_j+c\)\(dp_i-asum_i^2-bsum_i=-2asum_isum_j+asum_j^2-bsum_j+c\),这样就得到了 \(b=y-kx\) 形式的递推式,可以发现我们的 \(b\) 是关于 \(i\) 的,\(y\) 是关于 \(j\)\(-kx\) 是与 \(i,j\) 都相关的一项,并且把前半部分当做 \(-k\),后半部分当作 \(x\)

之后只需要带入维护凸包即可。

int n;
int a,b,c;
ll sum[maxn],dp[maxn];
int q[maxn],head,tail;
inline ll X(int i){
    return sum[i];
}
inline ll Y(int i){
    return dp[i]+a*sum[i]*sum[i]-b*sum[i];
}
inline ll K(int i){
    return 2*a*sum[i];
}
inline ll B(int i){
    return dp[i]-a*sum[i]*sum[i]-b*sum[i]-c;
}
inline db slope(int i,int j){
    return (db)(Y(i)-Y(j))/(X(i)-X(j));
}
int main(){
    n=read();
    a=read(),b=read(),c=read();
    for(int i=1;i<=n;i++){
        sum[i]=sum[i-1]+read();
    }
    int head=1,tail=1;
    q[tail]=0;
    for(int i=1;i<=n;i++){
        while(headK(i)) head++;
        dp[i]=Y(q[head])-K(i)*X(q[head])+a*sum[i]*sum[i]+b*sum[i]+c;
        while(head

3.P2120 ZJOI2007 仓库建设

同样是几步走——前缀和——推式子——移项整理——维护凸壳。

3.四边形不等式优化dp

待更