【ybtoj高效进阶 21282】数字重组(DP)(数学)

数字重组

题目链接:ybtoj高效进阶 21282

题目大意

给你一个数组,再给出一个数 k,保证数组长度是 k 的倍数。
然后要你把数组分成 k 个集合,定义一个分法的价值是它 k 个集合的极差之和,然后要你找价值最小的分发,输出其价值。

思路

考虑从小到大枚举数字。

考虑先把 (k) 个集合弄出来,以它们的个数弄成一个 vector 作为状态。
那如果你接下来加的集合是空的,那我们就会有 (-a_i) 的贡献,如果你加了之后集合满了,就有 (a_i) 的贡献。(因为你数字是从小到大依次加入)
那显然我们发现顺序不同的仍是同一个集合,所以我们可以排序,以减小状态数。

但接着有一个问题,它要划分成集合,也就是不能有重复的数字被划入同一个集合中。
那我们考虑设 (f_{i,j,S}) 为搞定到第 (i) 个数,当前状态集合是 (S),放 (i) 之前 (i) 这个位置的数所在集合的最小大小是 (j)。那我们就选当前个数不超过 (j) 的来放即可,因为如果之前有了那现在它的个数肯定是 (j+1)

那我们就再转移,看看复杂度,首先要算的是集合的个数。
它相当于从 ((0,0)) 走到 ((k,frac{n}{k})) 的折线。
所以你就是把向右向上的操作随便放,就是组合数 (C_{k+frac{n}{k}}^k)

不难看出当 (k=sqrt{n}) 的时候,状态数最多,为 (C_{2sqrt{n}}^{sqrt{n}})

然后再乘上 (nk),复杂度可以过。

代码

#include<map>
#include<vector>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long

using namespace std;

int n, k, a[101];
map <vector<int>, ll> now, nxt;
vector <int> emp;
ll ans;

int main() {
//	freopen("num.in", "r", stdin);
//	freopen("num.out", "w", stdout);
	
	scanf("%d %d", &n, &k);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	
	sort(a + 1, a + n + 1);
	
	emp.push_back(k);
	for (int i = 1; i <= k; i++)
		emp.push_back(0);
	now[emp] = 0;
	for (int i = 1; i <= n; i++) {
		nxt.clear();
		
		for (map <vector<int>, ll> ::iterator it = now.begin(); it != now.end(); it++) {
			vector <int> A = (*it).first;
			ll B = (*it).second;
			for (int j = 1; j <= k; j++) {
				if (A[j] == n / k) continue;
				if (a[i] == a[i - 1] && A[j] > A[0]) continue;
				
				vector <int> toA = A;
				ll toB = B;
				toA[j]++;
				if (toA[j] == 1) toB -= a[i];
				if (toA[j] == n / k) toB += a[i];
				toA[0] = 0; sort(toA.begin(), toA.end());//记得重新排序
				toA[0] = A[j];
				
				if (nxt.find(toA) == nxt.end()) nxt[toA] = toB;
					else nxt[toA] = min(nxt[toA], toB);
			}
		}
		
		swap(now, nxt);//滚动数组
	}
	
	ans = 1e15;
	for (map <vector<int>, ll> ::iterator it = now.begin(); it != now.end(); it++) {
		ans = min(ans, (*it).second);
	}
	printf("%lld", ans);
	
	return 0;
}
原文地址:https://www.cnblogs.com/Sakura-TJH/p/YBTOJ_GXJJ_21282.html