有个长度小于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;
}