线性动态规划的归纳总结

前言

  • 线性 dp :具有线性 “阶段” 划分的动态规划算法。
  • 无论线性 dp 的状态表示上是一维的还是多维的,都是线性上的递推。dp 的阶段沿着各个维度线性增长,从一个或多个边界点开始,有方向地向整个状态空间转移、拓展。最后每个状态上都保留了以自身为 “目标” 的子问题的最优解。

LIS问题

例题1:最长上升子序列(模板)

  • 题目描述: 给定 n 个数 ai,求数值单调递增的子序列的最长长度为多少。
  • 状态表示: f [ i ] f[i] f[i] :以 a i a_i ai 为结尾的最长上升子序列的长度
  • 转移方程: j ∈ [ 1 , i ) , a j < a i : f [ i ] = m a x ( f [ j ] + 1 ) j\in[1,i),a_j<a_i:f[i]=max(f[j]+1) j[1,i),aj<ai:f[i]=max(f[j]+1)
  • 初始值: f [ i ] = 1 f[i]=1 f[i]=1

方法1,O(n2):暴力

int main() {
	int ans=0;
	for(int i=1; i<=n; i++) {
		f[i]=1;
		for(int j=1; j<i; j++) {
			if(a[i]>a[j])f[i]=max(f[j]+1,f[i]);
		}
		ans=max(ans,f[i]);
	}
	cout<<ans;
}

方法2:O(nlogn) 树状数组

  • 很显然一个偏序关系关系。树状数组求前缀最大值即可。
void run(int n){
	for(int i=1;i<=n;i++){
	    f[i]=query(a[i]-1)+1;
	    update(a[i],f[i]);
	}
}

例题2:最长不上升/不下降子序列

  • 题目描述: 登山的过程中,有 n 个景点,每个景点的高度为 ai。你希望尽可能浏览多的景点,但是你有个习惯,就是一旦开始下山,就不能再往上走了。n<=1000
  • 问题分析:
  • 很显然,就是先非严格单调递增往上走,到达一个点,然后非严格单调递减往下走。要是我们能求出到达每个点的最长不下降子序列的长度,和每个点往后可以达到的最长不上升子序列长度就好了。
  • 前者可以直接模板 O(n2) 求,后者可以转化为逆向的最长不下降子序列长度,然后也模板 O(n2) 求 。
int main() {
	for(int i=1;i<=n;i++){
		f[i]=1;
		for(int j=1;j<i;j++){
			if(a[i]>=a[j])f[i]=max(f[i],f[j]+1);
		}
	}
	for(int i=n;i>=1;i--){
		g[i]=1;
		for(int j=n;j>i;j--){
			if(a[i]>=a[j])g[i]=max(g[i],g[j]+1);
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++)ans=max(ans,f[i]+g[i]-1);
}

例题3:思维转化

  • 题目描述: 一个东西朝向的河,有 n 个城市在北岸,n个城市在南岸。有 n 对友好城市关系 ,即北岸的城市 i i i 与南岸的城市 a i a_i ai 是友好城市关系。每对友好城市之间可建一条航道,但是航道之间不能交叉,求最多能建多少条航道。n<=1000
  • 问题分析: 对于北岸的城市 i,j ,当且仅当 i<j 并且 a i < a j a_i<a_j ai<aj 时不交叉。!!!这不就是求最长上升子序列的长度吗!!!

例题4:最长上升子序列的方案数

例题链接

  • 题目描述: 给定 n 个数 a i a_i ai,求最长上升子序列的长度 len ,以及有多少个长度为 len 的上升子序列。
  • 状态表示:
  • f [ i ] f[i] f[i] :以 a i a_i ai 为结尾的最长上升子序列的长度
  • g [ i ] g[i] g[i]:以 a i a_i ai 为结尾的上升子序列长度为 f [ i ] f[i] f[i] 的子序列个数
  • 初始状态: f [ i ] = g [ i ] = 1 f[i]=g[i]=1 f[i]=g[i]=1
  • 转移方程:
  • j ∈ [ 1 , i ) , a j < a i j\in[1,i),a_j<a_i j[1,i),aj<ai
  • f [ j ] + 1 = f [ i ] : g [ i ] + = g [ j ] f[j]+1=f[i]:g[i]+=g[j] f[j]+1=f[i]g[i]+=g[j]
  • f [ j ] + 1 < f [ i ] : f[j]+1<f[i]: f[j]+1<f[i] 无事发生
  • f [ j ] + 1 > f [ i ] : g [ i ] = g [ j ] , f [ i ] = f [ j ] + 1 f[j]+1>f[i]:g[i]=g[j],f[i]=f[j]+1 f[j]+1>f[i]g[i]=g[j],f[i]=f[j]+1
  • 【补充】:最后统计一下就可。
int main() {
	for(int i=1; i<=n; i++) {
		f[i]=g[i]=1;
		for(int j=1; j<i; j++) {
			if(a[j]<a[i]) {
				if(f[j]+1==f[i])g[i]+=g[j];
				else if(f[j]+1>f[i]) {
					f[i]=f[j]+1;
					g[i]=g[j];
				}
			}
		}
	}
	long long res=0;
	for(int i=1; i<=n; i++)ans[f[i]]+=g[i];
	for(int i=n; i>=1; i--) {
		if(ans[i]!=0) {
			cout<<i<<" "<<ans[i]<<endl;
			return 0;
		}
	}
}

例题5:每个元素出现在最长上升子序列的概率

  • 题目描述: n 个元素 a i a_i ai ,求每个元素出现在最长上升子序列的概率。 1 < = n < = 3000 1<=n<=3000 1<=n<=3000。例如:6 5 4 7,最长上升子序列有 (6,7),(5,7),(4,7),每个元素出现的概率分别为 0.33,0.33,0.33,1 。
  • 问题分析:
  • f 1 [ i ] / f 2 [ i ] f1[i]/f2[i] f1[i]/f2[i] 为以 i 结尾的最长上升/下降子序列长度
  • g 1 [ i ] / g 2 [ i ] g1[i]/g2[i] g1[i]/g2[i] 为对应达到最长上升/下降子序列的个数
  • 最长上升子序列的个数为 s u m = ∑ i = 1 n g 1 [ i ] [ f 1 [ i ] = L C S ] sum=\sum_{i=1}^n g1[i][f1[i]=LCS] sum=i=1ng1[i][f1[i]=LCS]
  • 一定出现在最长上升子序列的元素满足: f 1 [ i ] + f 2 [ i ] − 1 = L C S f1[i]+f2[i]-1=LCS f1[i]+f2[i]1=LCS
  • 其对应出现在最长上升子序列的概率为: g 1 [ i ] × g 2 [ i ] s u m \frac{g1[i]\times g2[i]}{sum} sumg1[i]×g2[i]
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+100;

long long f1[N],f2[N],g1[N],g2[N],LCS=-1e9,a[N],b[N];
int main() {
	int n;
	cin>>n;
	for(int i=1; i<=n; i++)cin>>a[i];
	for(int i=1; i<=n; i++) {
		f1[i]=g1[i]=1;
		for(int j=1; j<i; j++) {
			if(a[i]>a[j]) {
				if(f1[j]+1==f1[i])g1[i]+=g1[j];
				else if(f1[j]+1>f1[i]) {
					f1[i]=f1[j]+1;
					g1[i]=g1[j];
				}
			}
			if(LCS<f1[i])LCS=f1[i];
		}
	}
	for(int i=n; i>=1; i--) {
		f2[i]=g2[i]=1;
		for(int j=n; j>i; j--) {
			if(a[i]>a[j]) {
				if(f2[j]+1==f2[i])g2[i]+=g2[j];
				else if(f2[j]+1>f2[i]) {
					f2[i]=f2[j]+1;
					g2[i]=g2[j];
				}
			}
		}
	}
	long long sum=0;
	for(int i=1; i<=n; i++) {
		if(f1[i]==LCS)sum+=g1[i];
	}
	for(int i=1; i<=n; i++) {
		if(f1[i]+f2[i]-1==LCS)cout<<1.0*g1[i]*g2[i]/sum<<" ";
		else cout<<0<<" ";
	}
	return 0;
}

例题6:思维+不下降子序列的最大和

例题链接

  • 题目描述: 给定 n 个数 a i a_i ai ,要求通过将数字重新摆放位置的规则,使得这 n n n 个数从小到大递增。若将数字 a i a_i ai 重新摆放了一次,则消耗为 a i a_i ai 。求出这个消耗值的最小值。例如:7 1 2 3,方法1:移动7到末尾,消耗值为 7 ;方法2:分别移动 1 2 3 到开头,消耗值为 1+2+3 = 6 。 n < = 1000 , k i ∈ [ − 1 e 7 , 1 e 7 ] n<=1000,k_i\in[-1e7,1e7] n<=1000,ki[1e7,1e7]
  • 问题分析: 显然一个数最多只会被重新摆放一次。假设存在一个不下降子序列,则只需要将除了该不下降子序列的元素重排即可,消耗值为他们的和。问题转化为:求得一个不下降子序列,使得他们的和最大!!!
void solve() {
	long long sum=0,val=-1e9;
	for(int i=1; i<=n; i++) {
		f[i]=a[i];
		sum+=a[i];
		for(int j=1; j<i; j++) {
			if(a[j]<=a[i]) {
				f[i]=max(f[i],f[j]+a[i]);
			}
		}
		val=max(val,f[i]);
	}
	cout<<sum-val<<endl;
}

例题7:广义,偏序求最长子序列

例题链接

  • 题目描述: 给定 n 个字符串,求得一个最长子序列,使得这个子序列中,前一个均是后一个的前缀。 n < = 2 e 3 n<=2e3 n<=2e3
  • 问题分析: 前缀关系其实是满足一个二维偏序关系的。可以用 O ( n 2 × l e n ( s ) ) O(n^2\times len(s)) O(n2×len(s)) 的时间复杂度完成。
int check(int x,int y){
	if(a[x].length()>a[y].length())return 0;
	for(int i=0;i<a[x].length();i++){
		if(a[x][i]!=a[y][i])return 0; 
	}
	return 1;
}
int main(){
	for(int i=1;i<=n;i++){
		f[i]=1;
		for(int j=1;j<i;j++){
			if(check(j,i))f[i]=max(f[i],f[j]+1);
		}
		ans=max(ans,f[i]);
	} 
	cout<<ans;
}

LCS问题

例题1:最长公共子序列(模板)

  • 题目描述: 给定两个长度分别为 n,m 的字符串 A,B,求最长公共子序列的长度。n,m<=1000
  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j] :A 的前 i 个字符 B 的前 j 个字符的最长公共子序列的长度
  • 转移方程:
  • A [ i ] ≠ B [ j ] A[i]\not=B[j] A[i]=B[j] f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) f[i][j]=max(f[i-1][j],f[i][j-1]) f[i][j]=max(f[i1][j],f[i][j1])
  • A [ i ] = B [ j ] A[i]=B[j] A[i]=B[j] f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 f[i][j]=f[i-1][j-1]+1 f[i][j]=f[i1][j1]+1
  • 初始值: f [ i ] [ 0 ] = f [ 0 ] [ j ] = 0 f[i][0]=f[0][j]=0 f[i][0]=f[0][j]=0
  • 时间复杂度: O(nm)

Code

int main() {
	cin>>A+1>>B+1; 
	for(int i=0;i<=n;i++)f[i][0]=0;
	for(int j=0;j<=m;j++)f[0][j]=0;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			if(A[i]!=B[j])f[i][j]=max(f[i-1][j],f[i][j-1]);
			else f[i][j]=f[i-1][j-1]+1;
		}
	}
	cout<<f[n][m];
}

例题2:最长公共上升子序列

矩阵路径问题(只能往右或往下)

例题1:矩阵最大取值

  • 题目描述: 一个大小为 n × m n\times m n×m 的矩阵,其第 i 行第 j 列的权值为 a i j a_{ij} aij,从左上角到右下角走,每次只能向右走或向下走,经过一个点就会将该点的权值取走,求能取到的和的最大值为多少。n,m<=1000
  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j]:到达第 i 行第 j 列时,能取到的最大值之和。
  • 转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) + a [ i ] [ j ] f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j] f[i][j]=max(f[i1][j],f[i][j1])+a[i][j]
  • 初始值: f [ 1 ] [ 1 ] = a [ 1 ] [ 1 ] f[1][1]=a[1][1] f[1][1]=a[1][1]
  • 时间复杂度: O(nm)

例题2:矩阵路径计数

  • 题目描述: 一个大小为 n × m n\times m n×m 的矩阵,有 q 个点 a i j a_{ij} aij 不能被到达。从左上角到右下角走,每次只能向右走或向下走,求一共有多少条路径。n,m<=1000,q<=nm,保证起点和终点可到达。
  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j]:一共有多少条路径到达第 i 行第 j 列
  • 转移方程:
  • a i j = 0 , f [ i ] [ j ] = 0 a_{ij}=0,f[i][j]=0 aij=0,f[i][j]=0
  • a i j = 1 , f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − 1 ] a_{ij}=1,f[i][j]=f[i-1][j]+f[i][j-1] aij=1,f[i][j]=f[i1][j]+f[i][j1]
  • 初始值: f [ 1 ] [ 1 ] = 1 f[1][1]=1 f[1][1]=1
  • 时间复杂度: O(nm)

例题3:多维矩阵取数

  • 题目描述: 一个大小为 n × m n\times m n×m 的矩阵,其第 i 行第 j 列的权值为 a i j a_{ij} aij,从左上角到右下角走,每次只能向右走或向下走,经过一个点就会将该点的权值取走(取走后该值变为0),求走两次,能取到的和的最大值为多少。n,m<=100。
  • 状态表示: 考虑两次一起走, f [ i ] [ x ] [ y ] [ u ] [ v ] f[i][x][y][u][v] f[i][x][y][u][v]:走了 i 步,第一次走到 (x,y),第二次走到 (u,v) ,能取到的最大值。
  • 转移方程: f [ i ] [ x ] [ y ] [ u ] [ v ] = m a x ( f [ i − 1 ] [ x − 1 ] [ y ] [ u ] [ v ] , f [ i − 1 ] [ x ] [ y − 1 ] [ u ] [ v ] , f [ i − 1 ] [ x ] [ y ] [ u − 1 ] [ v ] , f [ i − 1 ] [ x ] [ y ] [ u ] [ v − 1 ] ) + [ ( x , y ) ≠ ( u , v ) ] × ( a x y + a u v ) + [ ( x , y ) = ( u , v ) ] × ( a x y + a u v ) f[i][x][y][u][v]=max(f[i-1][x-1][y][u][v],f[i-1][x][y-1][u][v],f[i-1][x][y][u-1][v],f[i-1][x][y][u][v-1])+[(x,y)\not=(u,v)]\times (a_{xy}+a_{uv})+[(x,y)=(u,v)]\times (a_{xy}+a_{uv}) f[i][x][y][u][v]=max(f[i1][x1][y][u][v],f[i1][x][y1][u][v],f[i1][x][y][u1][v],f[i1][x][y][u][v1])+[(x,y)=(u,v)]×(axy+auv)+[(x,y)=(u,v)]×(axy+auv),第 i 步是由那四种状态转移而来 。

优化

  • 状态表示: 很显然 i = x+y = u+v,我们只需要知道 i,x,u 就可以知道 y = i-x ,v = i-u 了
  • 转移方程: f [ i ] [ x ] [ u ] = m a x ( f [ i − 1 ] [ x − 1 ] [ u ] , f [ i − 1 ] [ x ] [ u ] , f [ i − 1 ] [ x ] [ u − 1 ] , f [ i − 1 ] [ x ] [ u ] ) f[i][x][u]=max(f[i-1][x-1][u],f[i-1][x][u],f[i-1][x][u-1],f[i-1][x][u]) f[i][x][u]=max(f[i1][x1][u],f[i1][x][u],f[i1][x][u1],f[i1][x][u])

括号匹配

例题1:只有 ()[] 的括号匹配

例题链接

  • 题目描述: 给定一个只包含 ()[] 的字符串,求出一个最长合法括号匹配子串,并输出
  • 状态设置: f [ i ] f[i] f[i]​ 为前 i 个字符串中,以 s [ i ] s[i] s[i]​ 结尾的最长合法括号匹配子串的长度。
  • 转移1: s [ i ] s[i] s[i] 可以与 s [ i − 1 ] s[i-1] s[i1] 匹配,若匹配成功,则 f [ i ] = f [ i − 2 ] + 2 f[i]=f[i-2]+2 f[i]=f[i2]+2
  • 转移2: s [ i ] s[i] s[i]​ 可以与 s [ i − f [ i − 1 ] − 1 ] s[i-f[i-1]-1] s[if[i1]1]​ 匹配,若匹配成功,则 f [ i ] = f [ i − 1 ] + f [ i − f [ i − 1 ] − 2 ] + 2 f[i]=f[i-1]+f[i-f[i-1]-2]+2 f[i]=f[i1]+f[if[i1]2]+2
  • 此外: f [ i ] = 0 f[i]=0 f[i]=0
void solve(){
    f[0]=0;
    for(int i=1;i<s.length();i++){
    	f[i]=0;
    	if(check(i-f[i-1]-1,i))f[i]=max(f[i],f[i-1]+2+(i-f[i-1]-2>=0?f[i-f[i-1]-2]:0));
    	if(check(i-1,i))f[i]=max(f[i],f[i-2]+2); 
    	maxlen=max(maxlen,f[i]);
	}
	for(int i=0;i<s.length();i++){
		if(f[i]==maxlen){
			cout<<s.substr(i-maxlen+1,maxlen);
			break;
		}
	}
}

其他线性dp

归纳模式一(最后一个元素必选的状态设置)

模型总结

  • 模型: f [ i ] f[i] f[i] 为只有前 i 个东西情况下的最优值有时候是不利于转移的,设 f [ i ] f[i] f[i] 为只有前 i i i 个东西且最后一个东西是 i i i 的情况下的最优值。这样有利于转移
  • 例子: 可以枚举 j j j 作为上一次选择的东西进行转移,当相邻物品的有关系的时候,很有用。(其实最长上升子序列就是这种思路)

转移的方式形如下式:

void solve(){
	for(int i=1;i<=n;i++){
		for(int j=1;j<i;j++){
			//从状态j转移到状态i 
		}
	}
}

例题1:每次删除一个区间,求删除全部的最小花费

例题链接

  • 题目描述: 给定 n 个城墙,每个城墙的坚固值为 a i a_i ai,以及一个上界值 t t t 。需要将 n n n 个城墙全部击破。若单独击破第 i i i 个城墙,则需要花费 a i × n × n a_i\times n\times n ai×n×n;若连续击破第 i i i 个城墙到第 j j j 个城墙,则需要花费 ∑ k = i j a i × ( a i + a j ) \sum_{k=i}^j a_i\times (a_i+a_j) k=ijai×(ai+aj) ,前提是 ( a i + a j < = t ) (a_i+a_j<=t) (ai+aj<=t)。求最少花费。 n < = 1000 , a i < = 1000 n<=1000,a_i<=1000 n<=1000,ai<=1000
  • 状态设置 f [ i ] f[i] f[i] 为击破前 i i i 个城墙,需要的最少花费。
  • 问题分析: j j j i i i 为最后一次城墙击破。可以枚举 j j j 来 dp
  • 转移方程: f [ i ] = m i n ( f [ i ] , f [ j ] + ( p r e [ i ] − p r e [ j ] ) ∗ ( a i + a j ) ) f[i]=min(f[i],f[j]+(pre[i]-pre[j])*(a_i+a_j)) f[i]=min(f[i],f[j]+(pre[i]pre[j])(ai+aj))
void solve(){
	for(int i=1;i<=n;i++)pre[i]=pre[i-1]+a[i]; 
	for(int i=1;i<=n;i++){
		f[i]=a[i]*n*n+f[i-1];
		for(int j=0;j<i-1;j++){
			if(a[i]+a[j+1]>t)continue;
			f[i]=min(f[i],f[j]+(pre[i]-pre[j])*(a[i]+a[j+1]));
		}
	}
	cout<<f[n];
}

例题2:每次删除一个区间,求删除全部的最小花费

例题链接

  • 题目描述: 给定 n 个数字,分别为 a i a_i ai 。需要将这 n 个数全部删去。若单独删去第 i i i 个数,则需要花费 a i a_i ai ;若连续删去第 i i i 个数都第 j j j 个数,则需要花费 ( i − j + 1 ) × ∣ a i − a j ∣ (i-j+1)\times |a_i-a_j| (ij+1)×aiaj n < = 100 , a i < = 1000 n<=100,a_i<=1000 n<=100,ai<=1000
  • 状态设置: f [ i ] f[i] f[i] 为删去前 i i i 个数,需要的最少花费。
  • 问题分析: j j j i i i 为最后一次删数。可以枚举 j j j 来 dp
  • 转移方程: f [ i ] = m i n ( f [ i ] , f [ j ] + ( i − j ) ∗ a b s ( a i − a j ) ) f[i]=min(f[i],f[j]+(i-j)*abs(a_i-a_j)) f[i]=min(f[i],f[j]+(ij)abs(aiaj))
void solve(){
	for(int i=1;i<=n;i++){
		f[i]=f[i-1]+a[i];
		for(int j=0;j<i-1;j++){
			f[i]=max(f[i],f[j]+(i-j)*abs(a[i]-a[j+1]));
		}
	}
	cout<<f[n];
}

例题3:选择一个长度为 T 的子序列,使得某价值最大

  • 题目描述: n 本书,宽度分别为 a i a_i ai​ ,从中删去 T T T​ 本书,使得剩下的书中:不整齐度 ∑ i = 2 n − k ∣ b i − b i − 1 ∣ \sum_{i=2}^{n-k}|b_i-b_{i-1}| i=2nkbibi1​ 最小。
  • 问题分析: 删除 T T T​ 本书等价于留 n − T n-T nT​ 本书。设 f [ i ] [ k ] f[i][k] f[i][k]​ 为前 i 本书,留 k k k 本书,最后一本书留下的情况下,不整齐度最小为多少。用 j j j​​​ 枚举前一本留下的书即可。
void solve(){
	for(int i=1;i<=n;i++){
		for(int k=1;k<=i;k++){
			for(int j=1;j<i;j++){
				f[i][k]=min(f[j][k-1]+abs(a[j]-a[i]),f[i][k]);
			}
		}
		ans=min(ans,f[i][n-T]);
	} 
} 

归纳模式二(相邻选点)

  • 模型: 相邻两个点至少有一个点要选。
  • 状态表示: f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1] 未第 i 个点不选与选的情况下的最优值。
  • 转移方程:
  • 一维:
  • f [ i ] [ 0 ] = f [ i − 1 ] [ 1 ] f[i][0]=f[i-1][1] f[i][0]=f[i1][1]
  • f [ i ] [ 1 ] = m i n ( f [ i − 1 ] [ 0 ] , f [ i − 1 ] [ 1 ] + a [ i ] f[i][1]=min(f[i-1][0],f[i-1][1]+a[i] f[i][1]=min(f[i1][0],f[i1][1]+a[i]
  • 树:
  • f [ u ] [ 0 ] = ∑ f [ v ] [ 1 ] f[u][0]=\sum f[v][1] f[u][0]=f[v][1]
  • f [ u ] [ 1 ] = ∑ ( m i n ( f [ v ] [ 1 ] , f [ v ] [ 2 ] ) ) + a [ u ] f[u][1]=\sum(min(f[v][1],f[v][2]))+a[u] f[u][1]=(min(f[v][1],f[v][2]))+a[u]

归纳模式三(物品分配)

  • 模型: 将 m 个物品分配给 n 个人,其价值由分配的方式决定,例如:第 i 个人分配到 j 个物品得到的价值为 a [ i ] [ j ] a[i][j] a[i][j]

  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j] 为将 j 个物品分配给前 i 个人,所得到的最大价值

  • 转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − k ] + a [ i ] [ k ] ) , k ∈ [ 0 , j ] f[i][j]=max(f[i-1][j-k]+a[i][k]),k\in[0,j] f[i][j]=max(f[i1][jk]+a[i][k])k[0,j]

  • 特别的,当每个人只能分配一个物品,

  • 转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − 1 ] + a [ i ] [ 1 ] , f [ i ] [ k ] ) f[i][j]=max(f[i-1][j-1]+a[i][1],f[i][k]) f[i][j]=max(f[i1][j1]+a[i][1],f[i][k])

例题链接

#include<iostream>
#include<cmath>
using namespace std;
long long f[205][205],a[205],b[205],v[205][205];

int main(){
	int m,n;
	cin>>m>>n;
	for(int i=1;i<=n;i++)cin>>a[i]>>b[i];
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			v[i][j]=a[i]*pow(j,b[i]);
		} 
	} 
	for(int j=1;j<=m;j++)f[0][j]=1e9;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=1e9;
			for(int k=0;k<=j;k++){
				f[i][j]=min(f[i][j],f[i-1][j-k]+v[i][k]);
			}
			
		}
	}
	cout<<f[n][m];
	return 0;
} 

归纳模式四(线段覆盖)

经典的线段覆盖问题:一维坐标系中,已知n个线段的左右端点坐标

问题1:求不重合下的最长覆盖
设f[i]一维坐标上从 0~i,线段的最长不重合覆盖
vector存储以线段右端点为下标的左端点坐标
则有f[i]=max(f[vec[i][j]]+i-vec[i][j]),且f[i]由f[i-1]继承

例题

#include<iostream>
#include<cmath>
#include<vector>
using namespace std;

vector<int>vec[30005];
int f[30005];

int main(){
	int n,l,r;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>l>>r;
		vec[r].push_back(l);
	}
	for(int i=0;i<=30000;i++){
		if(i>0)f[i]=f[i-1];
		for(int j=0;j<vec[i].size();j++){
			f[i]=max(f[i],f[vec[i][j]]+i-vec[i][j]);
		}
	}
	cout<<f[30000];
	return 0;
} 

杂项

例题1:3 个人接任务的一维流动距离

例题链接

  • 题目描述: 三个人在一个 [ 1 , L ] [1,L] [1,L] 的一维数轴上,最初分别在 1,2,3 。n 个任务,每次任务在 xi 发生,需要选择一个人走到 xi 去工作。给定 g [ i ] [ j ] g[i][j] g[i][j] 为 i 走到 j 的距离。求走动距离和的最小值。 3 < = L < = 200 , 1 < = n < = 1000 , 0 < = c [ i ] [ j ] < = 2000 3<=L<=200,1<=n<=1000,0<=c[i][j]<=2000 3<=L<=200,1<=n<=1000,0<=c[i][j]<=2000
  • 状态表示: f [ i ] [ j ] [ k ] [ p ] f[i][j][k][p] f[i][j][k][p]:三个人分别在 i , j , k i,j,k i,j,k ,已经接完了 p 个任务下的最小值。时间复杂度: O ( L 3 n ) O(L^3n) O(L3n)
  • 转移方程:
  • f [ x ] [ j ] [ k ] [ p ] = m i n ( f [ x ] [ j ] [ k ] [ p ] , f [ i ] [ j ] [ k ] [ p − 1 ] + c [ i ] [ x ] ) f[x][j][k][p]=min(f[x][j][k][p],f[i][j][k][p-1]+c[i][x]) f[x][j][k][p]=min(f[x][j][k][p],f[i][j][k][p1]+c[i][x])
  • f [ i ] [ x ] [ k ] [ p ] = m i n ( f [ i ] [ x ] [ k ] [ p ] , f [ i ] [ j ] [ k ] [ p − 1 ] + c [ j ] [ x ] ) f[i][x][k][p]=min(f[i][x][k][p],f[i][j][k][p-1]+c[j][x]) f[i][x][k][p]=min(f[i][x][k][p],f[i][j][k][p1]+c[j][x])
  • f [ i ] [ j ] [ x ] [ p ] = m i n ( f [ i ] [ j ] [ x ] [ p ] , f [ i ] [ j ] [ k ] [ p − 1 ] + c [ k ] [ x ] ) f[i][j][x][p]=min(f[i][j][x][p],f[i][j][k][p-1]+c[k][x]) f[i][j][x][p]=min(f[i][j][x][p],f[i][j][k][p1]+c[k][x])
  • 问题分析: 三个人其实本质上没有区别,不需要区分。考虑做完了第 p 个任务后一定有一个人站在了 x p x_p xp 位置。不妨我们只记录另外两个人的位置。
  • 状态设置: f [ i ] [ j ] [ p ] f[i][j][p] f[i][j][p]:有两个人站在 i,j ,一个人站在 x[p] ,已经接完了 p 个任务下的最小值。 O ( L 2 n ) O(L^2n) O(L2n)
  • 转移方程:
  • f [ i ] [ j ] [ p ] = m i n ( f [ i ] [ j ] [ p ] , f [ i ] [ j ] [ p − 1 ] + c [ x [ p − 1 ] ] [ x [ p ] ] ) f[i][j][p]=min(f[i][j][p],f[i][j][p-1]+c[x[p-1]][x[p]]) f[i][j][p]=min(f[i][j][p],f[i][j][p1]+c[x[p1]][x[p]])
  • f [ i ] [ j ] [ p ] = m i n ( f [ i ] [ j ] [ p ] , f [ j ] [ x [ p − 1 ] ] [ p − 1 ] + c [ i ] [ x [ p ] ] ) f[i][j][p]=min(f[i][j][p],f[j][x[p-1]][p-1]+c[i][x[p]]) f[i][j][p]=min(f[i][j][p],f[j][x[p1]][p1]+c[i][x[p]])
  • f [ i ] [ j ] [ p ] = m i n ( f [ i ] [ j ] [ p ] , f [ i ] [ x [ p − 1 ] ] [ p − 1 ] + c [ j ] [ x [ p ] ] ) f[i][j][p]=min(f[i][j][p],f[i][x[p-1]][p-1]+c[j][x[p]]) f[i][j][p]=min(f[i][j][p],f[i][x[p1]][p1]+c[j][x[p]])
int c[205][205],f[205][205][1005],x[1005];
int main() {
	int T,L,n;
	cin>>T;
	while(T--) {
		cin>>L>>n;
		for(int i=1; i<=L; i++) {
			for(int j=1; j<=L; j++) {
				cin>>c[i][j];
			}
		}
		for(int p=1; p<=n; p++)cin>>x[p];
        
        for(int i=1;i<=L;i++){
        	for(int j=1;j<=L;j++)f[i][j][0]=1e9;
		}
		f[1][2][0]=0;
		x[0]=3;
		for(int p=1; p<=n; p++) {
			for(int i=1; i<=L; i++) {
				for(int j=1; j<=L; j++) {
					f[i][j][p]=1e9;
					f[i][j][p]=min(f[i][j][p],f[i][j][p-1]+c[x[p-1]][x[p]]);
					f[i][j][p]=min(f[i][j][p],f[j][x[p-1]][p-1]+c[i][x[p]]);
					f[i][j][p]=min(f[i][j][p],f[i][x[p-1]][p-1]+c[j][x[p]]);
				}
			}
		}
		int ans=1e9;
		for(int i=1; i<=L; i++) {
			for(int j=1; j<=L; j++) {
				ans=min(ans,f[i][j][n]);
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

例题2:最多 w 次的来回走,求最多得到的值

例题链接

  • 题目描述: n 天,m 次换位置的机会。每天两棵树中有且仅有一个苹果长在树下。初始时(第一天结束时)人在树 1 的下面。求 n 天后,最多得到多少个苹果。 n < = 1 e 5 , w < = 30 n<=1e5,w<=30 n<=1e5,w<=30
  • 状态设置: f [ i ] [ j ] [ 1 / 2 ] f[i][j][1/2] f[i][j][1/2]:为第 i 天结束时,已经使用了 j 次换位置机会的情况下,在树 1/2 下面,最多可以得到多少个苹果。
  • 初始值: f [ 1 ] [ j ] [ 0 / 1 ] = − 1 e 9 , f [ 1 ] [ 0 ] [ 1 ] = ( a [ 1 ] = = 1 ) f[1][j][0/1]=-1e9,f[1][0][1]=(a[1]==1) f[1][j][0/1]=1e9,f[1][0][1]=(a[1]==1)
  • 转移方程:
  • f [ i ] [ j ] [ 1 ] = m a x ( f [ i − 1 [ j ] [ 1 ] , f [ i − 1 ] [ j − 1 ] [ 2 ] ) + ( a [ i ] = = 1 ) f[i][j][1]=max(f[i-1[j][1],f[i-1][j-1][2])+(a[i]==1) f[i][j][1]=max(f[i1[j][1],f[i1][j1][2])+(a[i]==1)
  • f [ i ] [ j ] [ 2 ] = m a x ( f [ i − 1 ] [ j ] [ 2 ] , f [ i − 1 ] [ j − 1 ] [ 1 ] ) + ( a [ i ] = = 2 ) f[i][j][2]=max(f[i-1][j][2],f[i-1][j-1][1])+(a[i]==2) f[i][j][2]=max(f[i1][j][2],f[i1][j1][1])+(a[i]==2)
void solve(){
    for(int j=0;j<=w;j++)f[1][j][1]=f[1][j][2]=-1e9;
    f[1][0][1]=(a[1]==1);
	for(int i=2;i<=n;i++){
		f[i][0][1]=f[i-1][0][1]+(a[i]==1);
		f[i][0][2]=f[i-1][0][2]+(a[i]==2);
		for(int j=1;j<=w;j++){
			f[i][j][1]=max(f[i-1][j][1],f[i-1][j-1][2])+(a[i]==1);
			f[i][j][2]=max(f[i-1][j][2],f[i-1][j-1][1])+(a[i]==2);
		}
	}
	long long ans=-1e9;
	for(int j=0;j<=w;j++)ans=max(ans,max(f[n][j][1],f[n][j][2]));
	cout<<ans; 
}

例题3:枚举位的方式求最长相邻且不为零子序列

  • 题目描述: 给定一个 n 个数的数组 a i a_i ai ,求一个最长子序列 b i b_i bi 的长度 k k k,使得 b i & b i − 1 ! = 0 , i ∈ [ 2 , k ] b_i\&b_{i-1}!=0,i\in[2,k] bi&bi1!=0,i[2,k] b < = 1 e 5 , a i < = 1 e 9 b<=1e5,a_i<=1e9 b<=1e5,ai<=1e9
  • 问题分析:
  • 朴素的 O ( n 2 ) O(n^2) O(n2) 写法进行优化:
  • f [ i ] f[i] f[i] 应该由与 a i a_i ai 相与不为 0 的 a j a_j aj f [ j ] f[j] f[j]​ 得到。
  • 记录每一位为 1 的一些最优值,枚举 a i a_i ai 中的 ‘1’ 位即可。
void solve(){
    long long ans=-1e9;
    for(int i=1;i<=n;i++){//f[i]:以i结尾,能得到的最大长度 
        f[i]=1;
        for(int j=0;j<31;j++){
        	if((a[i]>>j)&1)f[i]=max(f[i],maxv[j]+1);
		}
		for(int j=0;j<31;j++){
			if((a[i]>>j)&1)maxv[j]=max(maxv[j],f[i]);
		}
		ans=max(ans,f[i]);
	}
	cout<<ans;
}

归纳模式六

例题1

例题2

归纳模式七(最长平衡子串)

模型总结:

  • 模型: 从长度为 n 的数组中,找出一个最长子串,使得该子串的属性A和属性B一样多。
  • 问题分析: 令属性 A = − 1 A=-1 A=1,属性 B = + 1 B=+1 B=+1,其他属性为 0 。对其求前缀和,问题就变成了求得一对 l , r l,r l,r,使得 s [ r ] = s [ l ] s[r]=s[l] s[r]=s[l] r − l + 1 r-l+1 rl+1 最大。用一个桶记录一下 s [ i ] s[i] s[i] 对应的最小 i i i,即 m [ s [ i ] ] = m i n ( m [ s [ i ] , i ) m[s[i]]=min(m[s[i],i) m[s[i]]=min(m[s[i],i),那么答案就为: m a x ( i − m [ s [ i ] ] + 1 ) max(i-m[s[i]]+1) max(im[s[i]]+1)

例题1:最长A=B的子串

例题链接

  • 题目描述: 给定一个只包含 R R R​ 和 G G G​ 的字符串,求一个最长子串,使得 R R R G G G 的数量一样多。
  • 问题分析: R = − 1 R=-1 R=1 G = + 1 G=+1 G=+1 。就是模板题了
int main(){
    cin>>a;
    int len=strlen(a);
    for(int i=0;i<len;i++){
    	if(a[i]=='R')s[i+1]=s[i]+1;
    	else s[i+1]=s[i]-1;
	}
	for(int i=0;i<=len+N;i++)m[i]=1e9;
	
	m[0+N]=0;
    for(int i=1;i<=len;i++){
 		ans=max(ans,i-m[s[i]+N]);
		m[s[i]+N]=min(m[s[i]+N],i);   	
	}
	cout<<ans;
	return 0;
}

二维地图搜索

例题1:求起点到终点的拐弯次数

例题链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值