题目
题意概要
有
m
m
m 个敌人,血量分别为
a
1
,
a
2
,
…
,
a
n
a_1,a_2,\dots,a_n
a1,a2,…,an 。你有
n
n
n 个士兵,每个士兵每轮可以造成一点伤害(使一个敌人血量减小一)。现在你要把它们划分成
m
m
m 个小队(小队可以没有人),然后为每个小队分别选择
t
t
t 个敌人,这个小队中的所有人会分别对这
t
t
t 个敌人各自造成一点伤害。请问,在能够使得所有敌人的血量
a
i
′
⩽
0
a'_i\leqslant 0
ai′⩽0 的前提下,
t
t
t 最小是多少?输出方案。
方案的输出方法:先输出 t t t,然后输出 s 1 , s 2 , … , s m s_1,s_2,\dots,s_m s1,s2,…,sm 表示每个小队的人数。显然 ∑ i = 1 m s i = n \sum_{i=1}^{m}s_i=n ∑i=1msi=n 且 s i ⩾ 0 s_i\geqslant 0 si⩾0 。最后输出 t t t 行,第 i i i 行有 m m m 个整数,分别代表每个小队选择的第 i i i 次攻击对象。
数据范围与提示
m
⩽
n
⩽
1
0
6
m\leqslant n\leqslant 10^6
m⩽n⩽106 且
∑
i
=
1
m
a
i
⩽
1
0
6
\sum_{i=1}^{m}a_i\leqslant 10^6
∑i=1mai⩽106 。提示一:认真阅读题面。提示二:不会因为输出量过大导致输出超时吗?
思路
不愧是 C F ( Construction Federation ) CF\;(\text{Construction Federation}) CF(Construction Federation) 的 3100 3100 3100 构造题,毫不含糊……我在这道题上花费了 2.5 h 2.5h 2.5h,还是只骗了 80 80 80 分,泪目了……
我一开始花了
0.5
h
0.5h
0.5h 想一个错误的题面(这也是 提示一
的来源)。一定要注意到,敌人数量和分组数量相同!毫无疑问这是一个重要条件。否则的话,你就会像我一样,想到
m
=
1
m=1
m=1 时只分一组,但是有若干敌人,显然取不到 下界
⌈
∑
i
=
1
m
a
i
n
⌉
\left\lceil{\sum_{i=1}^{m}a_i\over n}\right\rceil
⌈n∑i=1mai⌉
结果仔细读题,发现这个反例是假的。又随便找了几个例子,感觉这个下界总是可以取到的,目标变为构造出这个解!
B
t
w
\rm Btw
Btw,我以前做的构造题都是这种 “找到下界直接构造” 的题,所以说我就直接选择相信下界了……
贪心地想,我们知道了 t t t,只要让组数最小就行。那么我们就每次选一个最大的 x x x 使得 ∑ ⌊ a i x ⌋ ⩾ t \sum\lfloor{a_i\over x}\rfloor\geqslant t ∑⌊xai⌋⩾t,也就是最大的一个可以打 t t t 次而不浪费的分组大小。然后直接打。很可惜的是,我们没有理由相信这是正确的1。
从一种更 constructive \text{constructive} constructive 的角度入手——是不是说,我们可以用 i i i 组几乎荡平前 i i i 个人,然后拓展到 ( i + 1 ) (i+1) (i+1) 组消灭 ( i + 1 ) (i+1) (i+1) 个人呢?这个思路看上去就很好,也很符合正常逻辑;可是还是走不通。这时候过去 2 h 2h 2h 了吧?我还是蛮郁闷的——条条大路通死胡同!太糟了!
上面不过寥寥几段文字,看上去极平淡、极简单;实际上这样的试错是漫长而烦躁的。最讨厌的是,没有任何动机。不像这道题,能够很明显地说出一种贪心的估值;而且你直接告诉我们如何分组,我也不知道怎样构造解。反着走走不动,正着走走不通,你让我做啥咧!
我还没忘了这招:从最小的情况开始考虑。之前画过 m = 4 m=4 m=4 的情况,是试图用 “拓展法” 求解;现在我直接画最小的 m = 2 m=2 m=2 。显然我要选出一组,然后一直打第一个敌人,画到柱形图上,就是一个柱形不断重复去覆盖第一个敌人的柱形;到最后一步了,怎么办?为了不浪费,就要 把第二个人接上去。于是把第二个人的柱形跟第一个人的柱形相拼接起来……
我的天哪!我画出了什么?这个结构的得出,实在出乎我的意料。这个结构告诉我们:沿着敌人柱形的缝隙,将 n n n 切开 就是答案!
我重新描述一下这个图:将长度为 n n n 的区间不断重复,放在第二行,第一行是长度依次为 a i a_i ai 的区间。分组相当于将 n n n 割裂,目标是每一组都唯一属于第一行的某个区间(攻击一个敌人)。那么只要沿着 a i a_i ai 的间隙,将 n n n 割开,上面的条件就得到了满足!
一共只有 ( m − 1 ) (m-1) (m−1) 个间隙,最多切成 m m m 组。不足 m m m 组用 s i = 0 s_i=0 si=0 的空组补齐。
时间复杂度 O ( n + m + ∑ i = 1 m a i ) \mathcal O(n+m+\sum_{i=1}^{m}a_i) O(n+m+∑i=1mai),其中 ∑ i = 1 m a i \sum_{i=1}^{m}a_i ∑i=1mai 是输出的复杂度。
代码
然而,考场代码中,我将最后一个前缀和也当成了 “分割点”,然后就得了 80 p t s 80pts 80pts,我真的郁闷啊……
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cctype>
#include <vector>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long llong;
inline int readint(){
int a = 0, c = getchar(), f = 1;
for(; !isdigit(c); c=getchar())
if(c == '-') f = -f;
for(; isdigit(c); c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar(char((x%10)^48));
}
const int MAXN = 1000006;
bool cut[MAXN]; int b[MAXN];
int a[MAXN], top;
int main(){
int n = readint(), m = readint();
rep(i,1,m){
a[i] = readint()+a[i-1];
if(i != m) cut[a[i]%n] = true;
}
cut[n] = true; // the last end
for(int i=1,lst=0; i<=n; ++i)
if(cut[i]) b[++ top] = i-lst, lst = i;
while(top != m) b[++ top] = 0;
const int tot = (a[m]+n-1)/n;
a[m] = tot*n+1; // avoid overflow
printf("%d\n%d",tot,b[1]);
rep(i,2,m) putchar(' '), writeint(b[i]);
putchar('\n'); int p = 1, now = 0;
for(int round=1; round<=tot; ++round){
for(int i=1; i<=m; ++i){
writeint(p), putchar(' ');
if((now += b[i]) == a[p]) ++ p;
}
putchar('\n'); // end of line
}
return 0;
}
后记
我尚不清楚,那个贪心做法究竟是正确还是错误的;但是旧神 F i r e W i n g B i r d \sf FireWingBird FireWingBird 直接把它切了!蓝条没空之前「卷毛句」绝不后退!
但是有人选择了相信一种更疯狂、更美好的可能性。然后 O U Y E \sf OUYE OUYE 就过了…… ↩︎