POJ 3709 K-Anonymous Sequence (斜率优化DP)

题目类型  斜率优化DP

题目意思
给一个由 n (2 <= n <= 500000) 个数组成的非降序的数列 现在要把这个数列划分成若干组(每组最少 k 个数 (2 <= k <= n)
对于每组所有数字要变成这一组的最小那个数 (代价是两个数的差的绝对值) 问最小的代价是多少

解题方法
首先可以很容易得出朴素的状态转移方程 :
dp[i] = Min(dp[j] + sum[i] - sum[j] - a[j+1]*(i-j))  (i - j >= k)
(其中 dp[i] 表示把前 i 个数字划分完成后的最小代价, sum[i] 表示前 i 个数的和, a[i] 表示第 i 个数字)
由于 n 很大, 所以这个 O(n*n) 算法是会超时的, 因此需要进行优化
这里使用 斜率优化 这个方法 详细参看这篇论文中的例2->  浅谈数形结合思想在信息学竞赛中的应用

对于这道题 假设有3个合法的候选点x, y, z (其中 x < y < z) 供现在需要 求的 i 进行决策
假设 y 不比 x 差的话可以得到
dp[x] + sum[i] - sum[x] - a[x+1]*(i-x)  >=  dp[y] + sum[i] - sum[y] - a[y+1]*(i-y)
变形一下可以得到
(dp[x] - sum[x] + a[x+1]*x) - (dp[y] - sum[y] + a[y+1]*y) >= i*(a[x+1] - a[y+1])
既然 x 与 y 是候选点那么上式中只有 i 是会变的 如果当决策到 i 的时候 y 不比 x 差 那么当决策到 i 后面的点的时候 y 也不比 x 差
因为 (a[x+1]-a[y+1]) <= 0 的因此 i*(a[x+1] - a[y+1]) 的值只会更小 而上式左边的值是不变的 因此随着 i 的增大上式始终成立, 即 y 不比 x 差
为了方便观察设 A(x) = dp[x] - sum[x] + a[x+1]*x;  设 B(x) = a[x+1]
如果使用一个单调队列保存候选点 (队列的点在原序列中的下标保持单调递增) 假设队列头是 head 
那么如果 A(head) - A(head+1) >= i*(B(head) - B(head+1) 的话队列的首元素  head 可以出队了(因为head+1这个候选点永远不比head差)
当 B(head)不等于 B(head+1)的时候 ( A(head)-A(head+1) ) / ( B(head) - B(head+1) ) 其实就相当于 head 与 head+1这两个点决定的直线的斜率
利用这个斜率可以在当一个候选点从队列尾进队列时使一些点从队尾出列
当队列尾的两个元素 x, y 与准备新加入队列的点 z (其中 x < y < z) 满足以下条件时可以把 y 出队
( A(x) - A(y) ) / ( (B(x) - B(y) ) >= ( A(y) - A(z) ) / ( (B(y) - B(z) ) 
也就是说当 直线 xy 的斜率比 直线 yz的斜率大的时候候选点 y 已经没存在的必要了
因为如果 y 要有存在的价值它必须在某个决策的时候优于 x, 也即是  ( A(x) - A(y) ) / ( (B(x) - B(y) ) <= i (因为B(x) - B(y)是小于0的所以要变号)
这时因为又有  ( A(x) - A(y) ) / ( (B(x) - B(y) ) >= ( A(y) - A(z) ) / ( (B(y) - B(z) ) 即 ( A(y) - A(z) ) / ( (B(y) - B(z) ) 也是 <= i, 即 z 比 y 更优, y 没有存在的价值

这道题注意点:
1.候选点要延迟加入 即当决策完 i 这个点后加入的候选点是 i - (k-1), 因为下一个需要决策的变量 i+1所能用到的最大的候选点就是 i-(k-1), 而且这个候选点要 >= k 才是合法的 要不它的 dp[]值是非法的, 因为同一组元素个数要大于等于 k
2. 不等式中的除法要转换成乘法 否则会出现除 0 的问题
3.注意long long的使用, 计算过程中两个较大的 int 型数相乘会超出 int 的表示范围

参考代码 - 有疑问的地方在下方留言 看到会尽快回复的
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

typedef long long LL;

const int maxn = 5e5 + 10;

LL a[maxn], sum[maxn], dp[maxn];
int q[maxn];

LL A(LL x) { // 上面分析定义的 A, B函数
	return dp[x] - sum[x] + a[x+1] * x;
}

LL B(LL x) {
	return a[x+1];
}

int main() {
	freopen("in", "r", stdin);
	int t;
	scanf("%d", &t);
	while(t--) {
		int n, k;
		scanf("%d%d", &n, &k);
		for( int i=1; i<=n; i++ ) {
			scanf("%lld", &a[i]);
			sum[i] = sum[i-1] + a[i]; // sum[i]表示前 i 个数的和
		}
		int head = 0, tail = 0; // 队列的头指针和尾指针 当队列为空时 head == tail 
		q[tail++] = 0;             // 刚开始把 0 放进队列中作为边界值, 这些更新出来的某些 dp[i] 是不合法的, 下面会说明怎么避免使用不合法的dp[i]
		for( int i=1; i<=n; i++ ) {
			while(head + 1 < tail) { // 当队列中有元素时从队列首往右扫, 满足分析中的不等式的话就把队首元素出队
				int x = q[head], y = q[head+1];
				if(A(x)-A(y)>=i*(B(x)-B(y))) head++;
				else break;
			}
			dp[i] = dp[q[head]] + sum[i] - sum[q[head]] - a[q[head]+1] * (i - q[head]); // 用当前最优的队首元素来更新 dp[i]
			if(i-(k-1) >= k) { // 把合法的候选点加进队列中 当 i-(k-1) 这个值其实就是下一个要更新的变量i+1所能考虑的最大候选点,这个值要>=k才是一个合法的候选点, 否则它的 dp[]值是非法的, 因为一组至少要有 k 个元素, 少于 k 个元素的组计算出来的值是不能用的
				while(head + 1 < tail) {
					int x = q[tail-2], y = q[tail-1], z = i - (k-1); 
					if((A(x) - A(y)) * (B(y) - B(z)) >= (A(y) - A(z)) * (B(x) - B(y))) { // 满足不等式把当前队尾元素出队
						tail--;
					}
					else break;
				}
				q[tail++] = i - (k-1);
			}
		}
		printf("%lld\n", dp[n]);
	}
	return 0;
}



你可能感兴趣的:(动态规划)