Loading

题解:[P11311 漫长的小纸带]

P11311 漫长的小纸带

题意:

有一个长度 \(n\) 的序列 \(a\),将 \(a\) 分成若干段,使得所有段价值和最小,定义价值为一段内元素数量的平方。

思路:

显然能用动态规划来计算答案,设 \(f[i]\) 表示到第 \(i\) 个位置所获得的最小价值,考虑怎么转移。

最直接的就是从 \(1\)\(i-1\) 枚举断点 \(j\),设最后一段为 \(S\),有 \(f[i]=min(f[j]+|S|^2)\),很遗憾它的时间复杂度是 \(O(n^2)\) 的,过不了该题。

我们发现答案最劣是 \(n\),即每个数字各成一段。对于一段 \(S\) 的贡献是 \(|S|^2\) 那么显然 \(|S|\) 不应大于 \(\sqrt{n}\),那么转移只需从 \(1\)\(\sqrt{n}\) 枚举最后一段元素个数即可。并且在元素个数相同的情况下,最后一段的长度越长,答案越优。

那么我们就需维护从 \(i\) 开始向前有 \(j\) 个元素时的最长长度,因为 \(i\) 是顺序枚举的,那么我们只需考虑新加入元素在之前的最后一段是否出现过,若出现过不做改变,否则从前向后删直到删去一个元素为止,具体可以看代码。

注意:

  1. 因为此题 \(1\le a[i]\le10^9\),所以需要离散化。

  2. 有一种写法是用一个类似桶的东西维护特定元素个数的最后一段的最长长度,大体代码长这样:

if(++cnt[j][a[i]]==1) {
	if(++C[j]>j) {
		while((--cnt[j][a[pos[j]]])!=0) pos[j]++;
		pos[j]++, C[j]--;
	}
}

不知为何经过一番卡常我也没过(也有可能是我的问题)。

  1. 此题有个双倍经验

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n, a[200005], pos[200005], pre[200005], last[200005], nex[200005], cnt[200005], f[200005];
//pos[i]表示最后一段有i个元素时的最长长度(我顺便用pos辅助了下离散化)
//cnt[i]表示pos[i]对应的元素个数 
//last[i]表示i的上一次出现位置 
//pre[i]表示与i位置相同的数的上一次出现位置,nex[i]表示下一次出现位置 
void discretization() { //离散化 
	ll cnt=0;
	map <ll, bool> vis;
	for(int i=1; i<=n; i++) if(!vis[a[i]]) {
		vis[a[i]]=1;
		pos[++cnt]=a[i];
	}
	sort(pos+1, pos+cnt+1);
	for(int i=1; i<=n; i++) a[i]=lower_bound(pos+1, pos+cnt+1, a[i])-pos;
}
int main() {
	cin >> n; 
	for(int i=1; i<=n; i++) cin >> a[i];
	discretization(); //离散化 
	for(int i=1; i<=n; i++) { //计算相关数组 
		pre[i]=last[a[i]];
		nex[last[a[i]]]=i;
		last[a[i]]=i;
		nex[i]=n+1;
	}
	memset(f, 0x3f, sizeof(f));
	memset(pos, 0, sizeof(pos));
	f[0]=0;
	ll t=sqrt(n);
	for(int i=1; i<=t; i++) pos[i]=1;
	for(int i=1; i<=n; i++) {
		for(int j=1; j<=t; j++) {
			if(pre[i]<pos[j]) cnt[j]++; //这个数上一次出现不在最后一段内 
			if(cnt[j]>j) { //元素个数超过j个 
				while(nex[pos[j]]<i) pos[j]++; //nex[pos[j]]<i说明在最后一段内该元素有一个以上,删去它不改变元素数量,否则改变 
				pos[j]++, cnt[j]--;
			}
			if(cnt[j]==j) f[i]=min(f[i], f[pos[j]-1]+j*j);
		}
	}
	cout << f[n];
	return 0;
}
posted @ 2024-11-24 21:00  Anins  阅读(32)  评论(0)    收藏  举报