题解_POJ3709_K匿名序列_斜率优化DP

链接link
题目大意:
定义如果对于每个节点 v,图中至少存在与 v 度相同的 k-1 个其他节点,我们称图为 k匿名序列。
现在给定一个按升序排列的度数序列,你唯一的操作是在任意一个数 -1,求最小减少多少次能把这个序列变成一个k匿名序列。

朴素DP

这是一个线性DP,看数据范围只能开出1维DP,所以直接定义dp[i]表示把前i个数变成k序列的最小步数。定义s[i]为前缀和,a表示原数组,则可以轻松的推出动态转移方程:
当1 <= i <= 2k时,dp[i] = s[i] - ar[1] * i
否则dp[i] = min{dp[j]+s[i]-s[j]+(i-j)*a[j+1]}(k <= j<= i - k)
到此,我们就获得了一个O(n2)算法

斜率优化

改变一下dp方程,发现其中有一项是 i * ar[j + 1],这样我们便会想到斜率优化
f[i]的值求的是范围内j算出来的最小值,换句话说,就是在范围内找一个值使这个值求出来的f[i]最小。而斜率优化就是在加速求这个最小值的时间。
由于加速的是求j的时间,所以我们先假设我们已经循环到了一个i,定义i为常量,j为变量。所以把常量丢到一边,变量丢到一边,就可以变换出下列方程:
f[j] - s[j] + ja[j + 1] = ia[j + 1] + f[i] - s[i]
定义:y = f[j] - s[j] + ja[j + 1]
k = i,x = a[j + 1], b = f[i] - s[i]
则原方程变换为了y = kx + b
这就像我们小学二年级 学过的一次函数。把这个方程看作一条直线,那么就是以x轴为a[j + 1],y轴为f[j] - s[j] + ja[j + 1]构建的平面直角坐标系中的一条直线。
ps:大佬可以跳过下面
那么问题来了,这条直线的本质是什么?
我第一次看斜率优化的时候就是这点不懂,好端端的动态转移方程为什么变成了一条直线?我们已经确定了i
就是我们的第一层循环。接下来我们把动态转移方程转换了形式,也就是说,我们第二次循环的j到这里也可以带入进这个方程中算出f[i]。至于这条直线,可以理解成每一组i和j动态转移。代入i和j后,算出截距(b),即可以算出是这组i,j所对应的f[i]。
现在,我们假设j是未知的,那么f[i]也是未知的。在确定i的情况下,我们只知道斜率。实际上,j确定的是解析式中的x和y,在一次函数解析式中,x和y就是这条直线上的一个点。也就是说,j的每一次选择可以理解成在直角坐标系中的一个点。每一次选择j就是选择这一个点,我们就可以得到这个点的x和y,再由转移方程(直线解析式)得到f[i]。几何语言:其实本质上就是在斜率确定的情况下,在范围内选出一个j使这条直线截距最小。
good
现在我们可以理解成这条直线向上平移知道碰到第一个点,那么这个点一定是范围内截距最小的点。不然就还有点在之前就已经碰到了。

插入

如果纯粹得循环选点,那时间复杂度和之前没有任何区别,所以我们就有了斜率优化。而这是真正让斜率优化提高时间复杂度的地方。我们每一次处理i,都会多一个选择。但是并不是每一个选择都有用,所以我们应该适当的摒弃一些选择。什么时候选择是没用的呢?
如同刚刚那张图,不管斜率有多大或者多小,中间那个点一定是不会被选到的。在直线向上平移的时候,一定会先碰到前面或者后面的那个点。直到中间那个点平移到像这样:

请添加图片描述
才会有直线经过他。不难看出,当二号点小于一号和三号点的连线时,一号和二号点所在直线的斜率,与二号点到三号点所在直线的斜率正好递增。我们称这种情况为下凸包,我们在插入的时候,就是要维护这种下凸包的结构。所以就非常好实现了。

选取

插入完成后,我们就要考虑选点求出f[i]了,毫无疑问,我们一定要选出能使直线截距最小的决策点(范围内j所对应x,y在平面内的点),也可以理解成直线向上平移所碰到的第一个点。请添加图片描述
先给结论:斜率为k的直线第一个碰到的点,这个点与前面一个点的斜率一定小于k,这个点与后面一个点的斜率一定大于k。看图说话,当k1 < k < k2时,这条直线一定先碰到二号点。在直线向上平移的过程中,如果把k2这条直线拉长,那么这条直线以及之后点的的直线与我们向上这条直线的交点,一定在这条线段的下方。这两个点组成线段中更左的那个点一定会比更右边的那个点先碰到直线。
与其相反,2号点左边的任意两个点组成的线段,更右的那个点一定会先碰到直线。请添加图片描述
在这个题中,有一个特殊的性质:由于我们从小到大循环i,所以斜率k,也就是i是单调递增的。要不然怎么说的模板题呢。 所以,当我们选出图中二号点的时候,下一次的点的编号一定比二号点大。由于平面直角系与我们所求直线的斜率都是递增的,所以以后的选择中,一定不会再选到1号点,所以可以出队了。

以上就是所有的分析,接下来奉上代码。

#include <iostream>
#include <cstdio>
#define int long long 
using namespace std;
const int N = 500005;
#define y(A) f[(A)] - s[A] + (A) * ar[(A) + 1]
//
//y = kx + f[i] - s[i]
//y - kx = f[i] - s[i]
//y - kx + s[i] = f[i] 
int f[N], ar[N], s[N];
int q[N], x[N], y[N];
int fr = 0, re = 0; 
signed main(){
//由dp方程推出来的公式:
//f[j] - s[j] + ja[j + 1] = ia[j + 1] + f[i] - s[i]  
	int T = 1;
	scanf("%lld", &T);
	while(T--){
  		for(int i = 0; i < N; ++i){//由于有多组数据,所以初始化 
			f[i] = ar[i] = s[i] = q[i] = x[i] = y[i] = 0;
		}
		fr = re = 0;
		
		int n, k;
		scanf("%lld%lld", &n, &k);
		if(k == 0){
			printf("0\n");
			continue;
		}
		for(int i = 1; i <= n; i++) f[i] = 0;
		for(int i = 1; i <= n; i++){
			scanf("%d", &ar[i]);
			s[i] = s[i-1] + ar[i];
		} 
//		for(int i = 1; i < k; i++) f[i] = s[i] - i * ar[1];
		
		//因为j的取值是 k ~ i - k,所以当i小于2k的时候没有选组 
		for(int i = 1; i < min(N, 2 * k); i++){
			f[i] = s[i] - i * ar[1];
			x[i] = ar[i + 1]; y[i] = y(i);
		}
		q[re++]=0;
		x[0] = 0;y[0] = y(0);//表示不分组的选择 
		for(int i = 2 * k; i <= n; i++){
			while(re - fr >= 2 && (y[q[re - 1]] - y[q[re - 2]]) * (x[i - k] - x[q[re - 1]]) >= 
				(y[i - k] - y[q[re - 1]]) * (x[q[re - 1]] - x[q[re - 2]])){
				re--;
			}//交叉相乘,斜率递增 
			
			q[re++] = i - k;//由于j的取值最大在i - k,所以加入的选择应该是i - k 	
//			while(re - fr >= 2 && (y[q[fr]] - y[q[fr + 1]]) / (x[q[fr]] - x[q[fr + 1]]) < i ){
//				fr++;
//			}
			//原本应该是上面这样。由于 (x[q[fr]] - x[q[fr + 1]])一定是非正数,所以移项之后方向改变 
			while(re - fr >= 2 && (y[q[fr]] - y[q[fr + 1]]) >= i * (x[q[fr]] - x[q[fr + 1]])){
				fr++;
			}
			int k = q[fr];
			f[i] = y(k) - i * ar[k + 1] + s[i];
			/*
			y = kx + f[i] -s[i]
			f[i] = y - kx + s[i]
			*/ 
			x[i] = ar[i + 1];
			y[i] = y(i);
		}
		printf("%lld\n", f[n]);
		 
	}
	return 0;
}
/* 
1
7 0
1 2 3 4 5 6 7
*/ 
/*
f[i] = f[j] + s[i] - s[i] + (i - j) * a[j + 1]
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值