DP的另一个角度 数列划分

有个长度小于40000数列,把它分成若干段,使得各段花费总和最小。一段的花费为该段不同元素的个数(不妨设有x)个,那么花费为x的平方。

看到题目,不难想到动态规划的做法。设f(i)为前i个元素最小的花费,然后枚举最后一段放多少个元素,不妨设为k个,那么f(i)=min{f(i-k-1)+cost}。这当然是可行的,但时间复杂度不容乐观。

再思考,发现每一段最多只有200个不同的元素,若大于200个,它的花费早已超过40000了,那还不如一个元素作为一段,花费还小于或等于40000。并且,在包括同样多不同元素的情况下,元素个数必然越多越好。比如:{1,2,2,1}就比{2,2,1}好,因为它们都有两个不同的元素,但前者有4个元素,后者只有3个。

所以f(i)的转移只需要枚举200个不同的f(j),j<i。比如有一个数列:1 2 1 3 2 2。现在求f(6)的值,枚举最后一段容纳多少个不同的元素,1个的时候是{2,2},2个的时候是{3,2,2},3个的时候是{1,2,1,3,2,2}。于是f(6)=min(f(4)+1^2,f(3)+2^2,f(0)+3^2),其中f(0)为哨兵,设为0。现在问题就是如何快速地找到j的位置,这很简单,用个预处理或简单的统计即可。时间复杂度:n*sqrt(n)。

从这题中可以知道DP的优化不一定都是用什么数据结构。可以从中发现一些规律。主要思考的如何快速求出当前状态的值。一般有三种手段:一是通过一些数据结构,例子数不胜数;二是通过前面已经求出来的值作为参考,整数拆分就是这类问题;三是像这题一样,通过减少转移的次数,来达到目标。

我是把所有的j位置找出来,其实没必要用链表……当然,还有其他方法,可以直接维护j的位置,因为i每次只加一,维护就容易了。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 40007;

int n, m;
int d[N], f[N];
int set[N], size;

const int P = 8000007;
int from[N], to[P], next[P], val[P], nedge;

void Insert(int a, int b, int c)
{
	to[++ nedge] = b;
	next[nedge] = from[a];
	val[nedge] = c;
	from[a] = nedge;
}

int main() 
{
	freopen("a.in", "r", stdin);
	freopen("a.out", "w", stdout);
	
	scanf("%d%d\n", &n, &m);
	
	for (int i = 1; i <= n; i ++)
	{
		scanf("%d\n", &d[i]);
		f[i] = i;
	}
	
	int forj = min((int) floor(sqrt(n)), m);
	for (int j = 1; j <= forj; j ++)
	{
		for (int i = 1; i <= m; i ++) set[i] = 0;
		size = 0;
		int cur = 1;
		
		for (int i = 1; i <= n; i ++)
		{
			for (; cur <= n && (size < j || set[d[cur]]); cur ++)
			{
				if (set[d[cur]] == 0) size ++;
				set[d[cur]] ++;
			}
			//f[cur - 1] <?= f[i - 1] + j * j;
			Insert(cur - 1, i - 1, j * j);
			
			set[d[i]] --;
			if (set[d[i]] == 0) size --;
		}
	}
	
	for (int i = 1; i <= n; i ++)
		for (int e = from[i]; e; e = next[e])
			f[i] <?= f[to[e]] + val[e];
	
	printf("%d\n", f[n]);
	
	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值