难度:省选/NOI-

题目描述

        有n个小朋友要进行比赛,他们要被分为若干队伍。每一个小朋友都有一个要求,其中第i个小朋友要求他所在的队伍最少要有$a_i$个人(包括自己)。
        现在请你求出一种划分方案,在满足所有小朋友的要求的情况下,最大化队伍的数量。同时在此基础上,请你最小化人数最多的队伍的人数。

输入格式

        第一行一个数n表示小朋友的个数。
        接下来n行,每行一个数,其中第i行的数字为$a_i$​。

输出格式

        第一行一个数t,表示在你的方案中的队伍数量。
        接下来t行,每行若干个空格隔开的数字,表示一只队伍。每一行首先输出一个数$k_i$ 表示第i只队伍的人数,接下来$k_i$个数依次描述该队伍内的小朋友的编号(从1开始)。
        若有多解(在满足题目要求的情况下),输出任意一个即可。

输入输出样例

Sample input

5
2
1
2
2
3

Sample output

2
2 4 2
3 5 1 3

说明

        $1\leq N\leq 1000000$,保证有解。

题解

        一开始以为是个傻X贪心,结果WA了后发现并不是这样。hack:8 1 2 4 5 5 5 5 5。
        首先将这些要求升序排序,可以发现最终的分组一定是一块一块的。那么考虑$DP(i)$为前$i$组成的最多组数,那么有状态转移方程:

        这里的$a_i$是升序后第$i$个的要求,和题目中的有所区别。这个方程是很好理解的。当不能组成组时,$DP$值设置成负无穷大即可。
        算法是$O(n^2)$的,但是发现最值可以拿前缀最大值优化,这样就是$O(n)$复杂度了。求最大组数不是问题,但是怎么保证最大值最小?
        看见最大值最小考虑二分,这里的二分比较有意思,是对$DP$过程做一些修改。假设现在要判定答案$x$的正确性,那么每一个组的人数都不能多于$x$个,在DP转移时把方程改一下就好了:

        然后看看最后的$DP(n)$是不是之前求出的最大组数,如果是,答案可行。
        处理一下边界,这个方程是很好推的,但是复杂度没法降维了。由于我们是从DP值最大的那一个转移,不妨开一个数组g保存到当前位置,出现过的最大的DP值对应下标的最大值。考虑到全局最优必然需要子结构最优(这是DP的原理),因此如果想要最后的$DP(n)$达到最大,这些$DP(i)$都应该达到最大,于是只考虑最大的转移,这也是算法正确性的保证。
        找到这个最大值出现的下标,如果它在范围里,则转移,否则$DP(i)$为无穷大,即无法转移(没法取到最大值,最终结果肯定不是最优的)。
        方案也容易得,在转移时记录一下转移的下标即可。
        于是复杂度降为$O(n)$,再加上二分的复杂度,总复杂度$O(nlogn)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <bits/stdc++.h>

#define N 1000005
#define inf 0x3f3f3f3f
using namespace std;


int dp[N], a[N], rk[N], n, b[N], g[N], e[N];

bool cmp(int x, int y) {
return a[x] < a[y];
}

int check(int x) {
for (int i = 1; i <= n; i++) {
int j = i - b[i];
if (j < 0)dp[i] = -inf, e[i] = -1;
else if (j == 0)dp[i] = 1, e[i] = 0;
else if (i - g[j] <= x)dp[i] = dp[g[j]] + 1, e[i] = g[j];
else dp[i] = -inf, e[i] = -1;
g[i] = dp[i] >= dp[g[i - 1]] ? i : g[i - 1];
}
return dp[n];
}

int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)scanf("%d", a + i), rk[i] = i;
sort(rk + 1, rk + n + 1, cmp);
for (int i = 1; i <= n; i++)b[i] = a[rk[i]];
int l = 0, r = n, mid, ans = check(n);
while (l < r) {//(,]
if (r == l + 1) {
check(r), printf("%d\n", ans);
for (int i = n; i >= 1;) {
printf("%d ", i - e[i]);
for (int j = i; j > e[i]; j--)printf("%d ", rk[j]);
i = e[i], printf("\n");
}
return 0;
}
mid = (l + r) >> 1;
if (check(mid) == ans)r = mid;
else l = mid;
}
}