线型DP呀

线型DP

杨老师的照相排列(轮廓线DP)

  • 题目大意:NNN 个同学合影,站成左对齐的 kkk 排,每排分别有 N1,N2,Nk...N_1,N_2,N_k...N1,N2,Nk... 个人(N1≥N2≥N3...N_1\ge N_2 \ge N_3...N1N2N3...
  • 第一排站在最后边,最后一排站在最前边,学生的身高互不相同,从高到低标记为 1,2...N1,2...N1,2...N
  • 合影要求按照每排从左到右身高递减,每列从后往前依次递减。
  • 问有多少种合法方案。
  • k≤5,N≤30k\le 5, N\le 30k5,N30

思路

首先,题目特别绕口,简单地说就是数字要求从左到右依次增大,从上到下依次增大,并且从上到下满足数量依次减少。

这道题目是蓝书DP第一道题目,在ACwing上也显示是简单题,当时就像拿来练手,不过现在看来,这像是一道劝退题,难度评定有误。

刚拿到这个题目,我们最先头疼的一定就是DP转移顺序

不知道你有没有和我一样的疑惑:

  • 第一个疑问:我该如何设置状态?平时见到二维平面DP都是 i,ji,ji,j 表示具体位置,或者直接表示 以 i,ji,ji,j 为结尾的矩形的答案值,这咋设,也不是二维平面,每一行都不一样
  • 第二个疑问:咋转移?转移的方向咋算?平时我们做的DP:1.常见的数列DP可以有明显的转移顺序,2.二维平面我们通常会考虑 [i,j−1][i,j-1][i,j1], [i−1,j][i-1,j][i1,j] ,[i−1,j−1][i-1,j-1][i1,j1] ,可是这个怎么考虑,一旦从这个角度开始考虑,首先卡柱的就是计算方案会重复,或者压根就不能转移。

这道题目也算是够新颖,学会了一种新的DP状态设定方法——轮廓集合DP

回到题目中来,对于这种填数,我们常规思路就是从1开始填,然后我们可以发现一个性质:

  • 每一行谈的数字的位置必须是连续的,换言之,不能提前占用后方格子,否则必然不会合法,因为后面的数字只会越来越大,被空下的位置上放上这样的数字必然不合法。

这也就是说:对于当前填的数字 kkk ,我们只能沿着当前状态下的边界放数。

说到这里,起码可以感受到平常DP的思路:连续地推,说的再普通一点就是通过上一个位置转移(常规的线型DP)

我们可以显然的发现,通过我们的常规填数法,一定可以得到所有的方案,这是显然的。然后我们又发现,对于当前的数kkk 我们只能放在已有状态的边界位置。我们只需要把边界位置描述出来就可以转移了

在这里插入图片描述

实际上就是把当前的轮廓表示出来就可以了。集合DP状态表示法来了…

可以发现 k≤5k\le 5k5 ,行数很少,直接5 维表示行数,这样轮廓就表示出来了,多余的行数设置成0,(我还是第一次见这样设置状态的)

f[a][b][c][d][e]f[a][b][c][d][e]f[a][b][c][d][e] 表示每一行有相应的 (a,b,c,d,e)(a,b,c,d,e)(a,b,c,d,e) 数量的方案数。

我们发现,通过从小到大依次放数的方式,对于当前数字 kkk,总满足当前状态下最大值,所以可以在边界所以放置,所以转移式子就出来了
f[a][b][c][d][e]=f[a−1][b][c][d][e]+f[a][b−1][c][d][e]+..... f[a][b][c][d][e]=f[a-1][b][c][d][e]+f[a][b-1][c][d][e]+..... f[a][b][c][d][e]=f[a1][b][c][d][e]+f[a][b1][c][d][e]+.....


在写代码的过程中,没有说一个点,就是必须满足 a≥b≥c≥d≥ea\ge b \ge c\ge d \ge eabcde ,因为上面写到,发现一个性质是每行必须是连续的,同样的,每列也必须是连续的,因此才有这样的限制条件。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int k;
int N[B];
int f[32][32][32][32][32];
void work()
{
	while (1)
	{
		cin>>k;
		if (k==0) break;
		for (int i=1;i<=5;i++) N[i]=0;
		for (int i=1;i<=k;i++) N[i]=read();
		f[0][0][0][0][0]=1;
		for (int a=0;a<=N[1];a++)
			for (int b=0;b<=N[2];b++)
				for (int c=0;c<=N[3];c++)
					for (int d=0;d<=N[4];d++)
						for (int e=0;e<=N[5];e++)
						{
							if (a==0 && b==0 && c==0 && d==0 && e==0);
							else f[a][b][c][d][e]=0;
							if (a>=b && b>=c && c>=d && d>=e);
							else continue;
							if (a-1>=0) f[a][b][c][d][e]+=f[a-1][b][c][d][e];
							if (b-1>=0) f[a][b][c][d][e]+=f[a][b-1][c][d][e];
							if (c-1>=0) f[a][b][c][d][e]+=f[a][b][c-1][d][e];
							if (d-1>=0) f[a][b][c][d][e]+=f[a][b][c][d-1][e];
							if (e-1>=0) f[a][b][c][d][e]+=f[a][b][c][d][e-1];
						}
		cout<<f[N[1]][N[2]][N[3]][N[4]][N[5]]<<"\n";
	}
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

最长公共上升子序列

  • 求出最长的严格上升公共子序列

思路

回顾LCS的解法,以及LIS的解法,我们可以发现二者的区别:

  • LCS的状态是 f[i][j]f[i][j]f[i][j] 表示A串前i个以及B串前j个的最长公共数量,没有对i,j的选取有特别的要求。
  • LIS的状态是 f[i]f[i]f[i] 表示第 iii 个位置必选的最长上升子序列,对位置 iii 有要求。

我在做这道题目的时候就发现,二者压根不能融合,因为,LIS必须强制选择,如何像LCS那样包含的答案是所有answer中的最优解的话,转移不具备正确性,可以很显然的看出,对于一个比最优解小的位置,其 F 值可以由其他位置更新,以此来得到此时位置的最优解。

一句话,LIS的答案是所有 f[i]f[i]f[i] 的最大值,而LCS的答案是 f[i][j]f[i][j]f[i][j]

我当时尝试了一下两边更改写法看看能不能融合,结果发现不行。

正解就比较神奇,果真就是融合,f[i][j]f[i][j]f[i][j] 表示A串前i个和B串前j个,并且B串第j个必须选的LCIS,我是直接强制i,j都选,现在想想错误很明显。

转移有:

  • a[i]==b[j] f[i][j]=max⁡{f[i−1][k]}+1f[i][j]=\max\{f[i-1][k]\}+1f[i][j]=max{f[i1][k]}+1
  • a[i]!=b[j] f[i][j]=f[i−1][j]f[i][j]=f[i-1][j]f[i][j]=f[i1][j]

可以发现是三维的,优化我也直接看了,因为我也不会。可以发现我们需要找到所有在 f[i−1]f[i-1]f[i1] 维度 a[i]>a[k]a[i]>a[k]a[i]>a[k] 的所有 kkk ,我们发现 a[i]a[i]a[i] 是固定的,当枚举 jjj 去更新 f[i][j]f[i][j]f[i][j] 的时候,顺便可以记录 f[i−1][k]f[i-1][k]f[i1][k] 的最大值,因此可以直接维护出来,时间复杂度 O(N2)O(N^2)O(N2)

#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int f[3009][3009];
int n;
int a[B];
int b[B];
void work()
{
	n=read();
	for (int i=1;i<=n;i++) a[i]=read();
	for (int i=1;i<=n;i++) b[i]=read();
	int maxx=0;
	for (int i=1;i<=n;i++)
	{
		maxx=0;
		for (int j=1;j<=n;j++)
		{
			if (a[i]==b[j])
			{
				f[i][j]=maxx+1;
			}
			else
				f[i][j]=f[i-1][j];
			
			if (b[j]<a[i])
			{
				maxx=max(maxx,f[i-1][j]);
			}
		}
	}
	maxx=0;
	for (int i=1;i<=n;i++)
	{
		maxx=max(f[n][i],maxx);
	}
	cout<<maxx;
}
int main()
{
	T=1;
	while (T--) work();
	return 0;
}

分级

  • 题目大意,构造一个B序列,满足
  • B非严格单调
  • 最小化 S=∑∣Ai−Bi∣S=\sum|A_i-B_i|S=AiBi
  • 只需要求出S即可
  • N≤2000,Ai≤106N\le 2000,A_i\le 10^6N2000Ai106

做题时思路:

不断地挖掘性质,首先确认这大体是什么算法,在没有蓝书的干扰下,首先想到的是构造,但是发现,最后的问法很奇怪:只需要求出S,说明数列并不是简单构造出来,而是在运行过程中因某些条件限制放置对应数,但是我们并不知道。这里首先想到的是贪心,或者某种性质,通过 for 不断确认位置。但是发现不可做。

然后想到的就是DP,因为DP的放置总是不知道如何放置的,但是最终结果却是最优解,所以我开始往DP方向想。

考虑到B是非严格单调,那么就有递增和递减两种情况,所以有两种状态设置,分别表示两种情况。

很明显的感受到,这个题就和LIS一样,必须强制了解位置才能转移。因为有一个明显误解就是当前的最优解不一定是下个最优解的转移原点。也就是说,有些位置单从那个点来说是吃亏的,但是全局是最优的。

所以我想出了 f[i][j]f[i][j]f[i][j] 表示前 iii 个点构造的B是递增的最后一个点选 jjj 的最小 SSS , 另一个DP方程相应的表示B组递减的最优解,然后最后两组答案取最大值即可。

可是发现 Ai≤106A_i\le 10^6Ai106 有点过大,不可行,并且这样暴力枚举数,很多是没有用的,所以可能不是从具体数字下手,不过那样又如何转移呢?????

哎,还是想不出来 ,看题解了

题解

基本上思路大差不大,很庆幸,居然对了大半部分,思路基本没错,两种状态,然后第二位是第 iii 位必须选的数字。优化就在于一个定理:

  • 至少存在一组最优解满足B数组的所有元素都来自A数组。

证明:

在这里插入图片描述

如上图,横轴表示A数组,纵轴表示A数组排序之后的 A‘A^`A 。假设蓝点是某种 BBB 序列结果,我们来观察紫色框中的情况,即三个点都不在直线上,而是在 A1,A2A_1,A_2A1,A2 (带撇,不会写)之间,如果如果把他们对应的 A3,A4,A5A_3,A_4,A_5A3,A4,A5 按照纵轴排序,可以发现,一部分在紫色框的上边,一部分在紫色框下边,这时候,如果我们将紫框中的元素同时平移,产生的贡献取决于两边点的数量多少,很显然,那边点多,就往哪边移动,直到其中有点到达边界。

因为蓝点是单调序列,那么必然是框中边界的点线到达直线,然后按照这个思路,剩余的点也如此操作,每个点最后只会停在直线上,不管是否是最优解,我们总能发现,在直线上总比在直线区间更优,或者说,在直线区间时,总能通过比较上下点数的贡献,来移动点,因为我们得出结论:

至少存在一组最优解满足B组中的所有元素都在A中出现过。

有了这个性质,我们数组的第二维直接将至 10310^3103 维度,剩下的就可LCIS类似。
f[i][j]=min⁡{f[i−1][k]+∣Ai−Aj∣}(a[j]≥a[k]) f[i][j]=\min\{f[i-1][k]+|A_i-A_j|\}(a[j]\ge a[k]) f[i][j]=min{f[i1][k]+AiAj}(a[j]a[k])
考虑如何优化,我尝试按照LCIS的优化方式去做,但是不行,这次 a[k]a[k]a[k]a[j]a[j]a[j] 有关,但和 a[i]a[i]a[i] 无关,所以不能直接套用。可以发现我们只需要比 a[j]a[j]a[j] 小的元素,并且我们在选择 AAA 的元素时,与A中的位置无关,只与大小有关。所以我们可以排序A数组,这时候我们发现,所寻找的 min⁡{f[i−1][k]}\min\{f[i-1][k]\}min{f[i1][k]} 实际上就是 jjj 的前缀 min⁡{f[i−1][1,j]}\min\{f[i-1][1,j]\}min{f[i1][1,j]} 。也就是前缀最值优化。

#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int n;
int a[B];
int f[2009][2009];
int dp[2009][2009];
int c[B],b[B];
int cmp(int a,int b) {return a>b;}
void work()
{
	cin>>n;
	for (int i=1;i<=n;i++) a[i]=read(),b[i]=c[i]=a[i];
	sort(b+1,b+1+n);
	sort(c+1,c+1+n,cmp);
	memset(f,0x3f3f3f3f,sizeof(f));
	memset(dp,0x3f3f3f3f,sizeof(dp));
	for (int i=1;i<=n;i++)
	{
		f[0][i]=0;
		dp[0][i]=0;
	}
	int ans1=0x3f3f3f3f,ans2=0x3f3f3f3f;
	for (int i=1;i<=n;i++)
	{
		int maxx=0x3f3f3f3f;
		for (int j=1;j<=n;j++)
		{
			int x=a[i]-b[j];
			if (x<0) x=-x;
			maxx=min(maxx,f[i-1][j]);
			f[i][j]=maxx+x;//优化不会... 
			if (i==n) ans1=min(ans1,f[i][j]);//会了,发现与位置没关系,排序前缀优化, 
		}	
	}
	for (int i=1;i<=n;i++)
	{
		int maxx=0x3f3f3f3f;
		for (int j=1;j<=n;j++)
		{
			int x=a[i]-c[j];
			if (x<0) x=-x;
			maxx=min(maxx,dp[i-1][j]);
			dp[i][j]=maxx+x;//优化不会... 
			if (i==n) ans2=min(ans2,dp[i][j]);
		}	
	}
	cout<<min(ans1,ans2); 
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值