[CF1119G]Get Ready for the Battle

题目

传送门 to CF

题意概要
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 ai0 的前提下, 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 si0 。最后输出 t t t 行,第 i i i 行有 m m m 个整数,分别代表每个小队选择的第 i i i 次攻击对象。

数据范围与提示
m ⩽ n ⩽ 1 0 6 m\leqslant n\leqslant 10^6 mn106 ∑ i = 1 m a i ⩽ 1 0 6 \sum_{i=1}^{m}a_i\leqslant 10^6 i=1mai106 。提示一:认真阅读题面。提示二:不会因为输出量过大导致输出超时吗?

思路

不愧是 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 ni=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 xait,也就是最大的一个可以打 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) (m1) 个间隙,最多切成 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 直接把它切了!蓝条没空之前「卷毛句」绝不后退!


  1. 但是有人选择了相信一种更疯狂、更美好的可能性。然后 O U Y E \sf OUYE OUYE 就过了…… ↩︎

Guess you like

Origin blog.csdn.net/qq_42101694/article/details/121335411