挖串 dp 笔记

概览

一类特殊的区间 dp,常常

  • 定义一种操作为删除当前序列的一个子段,子段两侧的数会变成相邻的
  • 每次删除操作有一个代价
  • 求删空序列的总代价最值

由于本题中的当前串的“子段”在被挖完串后其实不一定是连续段,所以普通的区间 dp 是不可行的。

办法

【例1】[THUSC2016]成绩单
设两个 dp 数组:\(f[l][r]\)\(g[l][r][mn][mx]\)

\(f[l][r]\) 表示区间 \([l,r]\) 取空的最小代价,则 \(f[1][n]\) 即为所求。
\(g[l][r][mn][mx]\) 表示取空 \([l,r]\),当最后一次取的最小值为 \(mn\)、最大值为 \(mx\) 时,最后一次之前最小的代价是多少。

\[f[l][r]=\min(g[l][r][mn][mx]+a+b(mx-mn)^2)\\ g[l][r][\min(mn,w[r])][\max(mx,w[r])]=\min(g[l][r-1][mn][mx])(1)\\ g[l][r][mn][mx]=\min(g[l][k][mn][mx]+f[k+1][r])(2) \]

含义:
\((1):\)
\((2):\)

https://loj.ac/s/1327848

【例2】[ACSX0212]好

首先观察转化题意,发现每次可以删的应该是一个先上升再下降的子段(当然也可以全升/全降),且每次上升1,每次下降1。先考虑全部删完(忽略可以不删完的条件),因为这是挖串dp擅长解决的问题。
走挖串流程。设一个\(f[l,r]\)表示l...r全部删空的最大价值。发现不太好继续做,因为不好确定最后删的子序列。
这时有一个很巧妙的分类讨论:假如l,r不是在同一次操作被删,那么就可以找到一个界点\(\in [l,r)\)使得\([l,k]\)\([k+1,r]\)成为两个子问题。

\[f[l,r]=\max_{k\in[l,r)}(f[l,k]+f[k+1,r]) \]

假如l,r是在同一次操作被删,令这次操作为最后一次操作,那么在[l,r]的峰顶k,接下来就是要知道[l,k]中把序列删到只剩一个上坡的最大价值,以及[k,r]中把序列删到只剩一个下坡的最大值,再加上这最后一次操作的价值v[2a[k]-a[l]-a[r]+1](原因是你知道了a[k],a[l],a[r]好序列长度就确定了)
设g[l,r][0]表示将[l,r]删到只剩一个上坡,并且l,r都在坡里的最大值;~[1]表示~下坡。

\[g[l,r][0]=\max_{k\in[l,r),a[k]=a[r]-1}(g[l,k][0]+f[k+1,r-1])\\ g[l,r][1]=\max_{k\in[l,r),a[k]=a[r]+1}(g[l,k][1]+f[k+1,r-1]) \]

上面是g的dp方程;f的转移:

\[f[l,r]=\max_{k\in[l,r],g[l,k][0]和g[k+1,r][1]有解}(g[l,k][0]+g[k+1,r][1]+v[2a[k]-a[l]-a[r]+1]) \]

回头看不一定要删完可以得到的最优收益。那么就是选择若干个不重合线段,每个线段有一个价值,求最大价值。这个可以\(O(n^2)\)dp.

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=405;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,a[N],v[N];
ll ans,f[N][N],g[N][N][2],h[N];
int main(){
    freopen("good.in","r",stdin);freopen("good.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>v[i];
	for(int i=1;i<=n;i++)cin>>a[i];
	memset(f,-0x3f,sizeof(f));
	memset(g,-0x3f,sizeof(g));
	for(int i=1;i<=n;i++)f[i][i]=v[1],g[i][i][0]=g[i][i][1]=0,f[i+1][i]=0;
	for(int len=2;len<=n;len++){
		for(int l=1,r=len;r<=n;l++,r++){
			for(int k=l;k<=r;k++){
				if(a[k]==a[r]-1&&g[l][k][0]>-INF)g[l][r][0]=max(g[l][r][0],g[l][k][0]+f[k+1][r-1]);
				if(a[k]==a[r]+1&&g[l][k][1]>-INF)g[l][r][1]=max(g[l][r][1],g[l][k][1]+f[k+1][r-1]);
			}
			for(int k=l;k<r;k++)f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]);
			for(int k=l;k<=r;k++)
				if(g[l][k][0]!=-INF&&g[k][r][1]!=-INF)
					f[l][r]=max(f[l][r],g[l][k][0]+g[k][r][1]+v[2*a[k]-a[l]-a[r]+1]);
		}
	}
	for(int i=1;i<=n;i++){
		h[i]=h[i-1];
		for(int j=0;j<i;j++) h[i]=max(h[i],h[j]+f[j+1][i]);
	}
	cout<<h[n];
}

挖串DP基本步骤(走流程)

  1. 识别挖串DP(通常是删掉一个子段能得到价值/代价,删后左右合并,要最优化收益)
  2. 观察性质,说出一次可以删的子序列的特征
  3. \(f[l,r]\)表示[l,r]删完的最优收益
  4. \(g[l,r][限制条件]\)表示[l,r]删到只剩一个满足限制条件的子序列能得到最优收益 要求:知道限制条件就能直接算得单次收益
  5. \(f[l,r]=g[l,r][限制条件]+\) 满足限制条件算得的单次收益

注云:这是绝主要脉络,是核心。但题目可能会在边角料上加工,需要通过分类讨论等方式找到这个核心。
WARNING: dp的初始值注意设为-INF,可以判定无解。

习题

这道题通过上述讲的方法相信大家都是会的。

\(f[l,r],g[l,r][i]\) 表示 \([l,r]\) 全删完的最大价值;\([l,r]\) 删到只剩 \(i\)\(a_r\) 的最大收益。

\[f[l,r]=\max(g[l,r][i]+i^2)\\ g[l,r][i]=\max_{k\in [l,r)}(g[l,k][i-1]+f[k+1][r-1]) \]

但通过这道题我们可以积累另一个 dp 状态设计方式,那就是设将来决策,以消除后效性
可以设 \(f[l,r][i]\) 表示:删完 \([l,r]\) 及从 \(r+1\) 往右数 \(i\)\(a_r\),能得到的最大价值。于是需要考虑我们这一次

  • 是直接把 \(a_r\) 以及右边那一串 \(=a_r\) 的全部“兑现”;
  • 还是攒着跟 \([l,r)\) 中的一些 \(=a_r\) 的一起消。

转移式分别为

\[f[l,r][i]=f[l,r-1][0]+(1+i)^2\\ f[l,r][i]=\max_{k\in [l,r)}(f[l,k][1+i]+f[k+1,r-1][0]) \]

Code1
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=205;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,a[N];
ll f[N][N],g[N][N][N];
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	memset(f,-0x3f,sizeof(f));
	memset(g,-0x3f,sizeof(g));
	for(int i=1;i<=n;i++)f[i][i]=1,g[i][i][1]=0,f[i][i-1]=0;
	for(int len=2;len<=n;len++){
		for(int l=1,r=len;r<=n;l++,r++){
			g[l][r][1]=f[l][r-1];
			for(int i=2;i<=r-l+1;i++)
				for(int k=r-1;k>=l;k--)
					if(a[k]==a[r]&&g[l][k][i-1]>-INF)
						g[l][r][i]=max(g[l][r][i],g[l][k][i-1]+f[k+1][r-1]);
			for(int i=1;i<=r-l+1;i++)
				if(g[l][r][i]>-INF)
					f[l][r]=max(f[l][r],g[l][r][i]+1ll*i*i);
		}
	}
	cout<<f[1][n]<<'\n';
}
int main(){int t;cin>>t;for(int i=1;i<=t;i++)printf("Case %d: ",i),solve();}
Code2
#include <bits/stdc++.h>
#define sq(a) (a)*(a)
using namespace std;
typedef long long ll;
const int N=205;
int n,a[N],f[N][N][N];
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int len=1;len<=n;len++){
		for(int l=1,r=len;r<=n;l++,r++){
			for(int i=0;i<=n-r;i++){
				f[l][r][i]=0;
				f[l][r][i]=max(f[l][r][i],f[l][r-1][0]+sq(1+i));
				for(int k=l;k<r;k++)
					if(a[k]==a[r])
						f[l][r][i]=max(f[l][r][i],f[l][k][1+i]+f[k+1][r-1][0]);
			}
		}
	}
	cout<<f[1][n][0]<<'\n';
}
int main(){
	int t;cin>>t;for(int i=1;i<=t;i++)printf("Case %d: ",i),solve();
}
posted @ 2021-12-22 17:35  pengyule  阅读(134)  评论(0)    收藏  举报