【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_{2\sqrt{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;
}