[ACNOI2021]《普林斯普再临》

280 篇文章 1 订阅

题目

题目背景
为了重新见到普林斯普 O n e I n D a r k \sf OneInDark OneInDark 从书架上找到了一本很古老的书,封面上用碳素墨水写下的书名,已经被彻底风化了,没有留下一丝痕迹,可是书却完好无损。即使是夏天,手放在书脊上时,也会如同触摸干冰,寒气彻骨,手指也会很快地黏在上面。

书好像被什么看不见的东西压住了,使出浑身气力,才能勉强翻开第一页。当书翻开的一瞬间,窗外的鸟竟一齐惊飞而去。低头看书,第一页的最顶上,用索莫兰语刻着一句话:知吾之人,入吾之门。逆吾之人,不复续存。

题目描述
普林斯普在书中,给出了一道难题:

给出 n n n 个数字 a 1 , a 2 , a 3 , … , a n a_1,a_2,a_3,\dots,a_n a1,a2,a3,,an,满足 ∑ a i \sum a_i ai 为偶数,求一组 c 1 , c 2 , c 3 , … , c n ∈ { − 1 , 1 } c_1,c_2,c_3,\dots,c_n\in\{-1,1\} c1,c2,c3,,cn{1,1} 使得 ∑ a i c i = 0 \sum a_ic_i=0 aici=0

后来,某一个宗教的人们——信仰 s c i e n c e \rm science science 这门宗教的——称呼这个问题为 N P \rm NP NP 。毫无疑问,他们都不配面见普林斯普,甚至不配为普林斯普所支配。

但是书页的角落里,留下了上一个「飞升者」的「祝福」:保证 { a 1 , a 2 , a 3 , … , a n } ⫅ { 1 , 2 , 3 , … , m } \{a_1,a_2,a_3,\dots,a_n\}\subseteqq\{1,2,3,\dots,m\} {a1,a2,a3,,an}{1,2,3,,m} ⌊ 2 m 3 ⌋ < n \lfloor\frac{2m}{3}\rfloor<n 32m<n

循着前人的脚步,我们终将成为「通晓者」!

数据范围与提示
3 ⩽ n ⩽ m ⩽ 1 0 6 3\leqslant n\leqslant m\leqslant 10^6 3nm106,且题目描述中的限制条件都将得到满足。

思路

据出题人 namespace_std \textsf{namespace\_std} namespace_std 说,这题是根据 M O MO MO 高联二试的某题改编的;然而我并没有找到。

这个真的非常 i n c r e d i b l e \rm incredible incredible,不太容易复刻。我只能尽力 假装它有动机

首先一个比较鲜明的想法是,数字越小越容易做到。因为这样可以保证值域在一个较小的范围内跳跃。虽然说也没有什么证据啊

那么怎么让权值减小呢?比如考虑最大值减去最小值么?然而我们完全没理由相信这可行……或者说, max ⁡ \max max 无论减谁都一样,那就减去次大值?如果是 A C M \rm ACM ACM 赛制(而且你有一颗勇敢的心,不怕罚时),交一发试试——通过了!我的马鸭!

很难想象。但是它确实没问题。而且可以给出证明。不妨设输入的 a a a 是有序的。

构造序列 d i = a 2 i − a 2 i − 1    ( i ⩽ n 2 ) d_i=a_{2i}-a_{2i-1}\;(i\leqslant\frac{n}{2}) di=a2ia2i1(i2n),为了方便,先假定 2 ∣ n 2\mid n 2n 吧。如果在数轴上考虑这个问题,你就会发现,一个 d i d_i di 相当于一个线段的长度,而这 n 2 \frac{n}{2} 2n 个线段是不相交的(即使端点也不相交),所以中间一定会有 ( n 2 − 1 ) (\frac{n}{2}-1) (2n1) 个长度至少为 1 1 1 的空隙,而总长是 [ 1 , m ] [1,m] [1,m] 的长度 ( m − 1 ) (m-1) (m1),所以
∑ d i ⩽ m − ⌊ n 2 ⌋ \sum d_i\leqslant m-\left\lfloor\frac{n}{2}\right\rfloor dim2n
为什么加了整除符号?是因为加入了 2 ∤ n 2\nmid n 2n 的情形——可以假定 a 0 = 0 a_0=0 a0=0,仍做差分,那就有 ⌊ n 2 ⌋ \lfloor{n\over 2}\rfloor 2n 个空隙了,但是线段总长是 [ 0 , m ] [0,m] [0,m] m m m,所以 ∑ d i ⩽ m − ⌊ n 2 ⌋ \sum d_i\leqslant m-\lfloor{n\over 2}\rfloor dim2n,也就是上面的式子。

继续 放缩,因为题目中 n n n 的范围肯定有用。原式 ⌊ 2 m 3 ⌋ < n \lfloor{2m\over 3}\rfloor<n 32m<n 等价于 2 m < 3 n 2m<3n 2m<3n,代入原式有
∑ d i ⩽ 1 2 ( 2 m − 2 ⌊ n 2 ⌋ ) < 1 2 [ 3 n − ( n − [ 2 ∤ n ] ) ] < 1 2 ( 2 n + [ 2 ∤ n ] ) \begin{aligned} \sum d_i &\leqslant\frac{1}{2}\left(2m-2\left\lfloor\frac{n}{2}\right\rfloor\right)\\ &<\frac{1}{2}\big[3n-(n-[2\nmid n])\big]\\ &<\frac{1}{2}\big(2n+[2\nmid n]\big) \end{aligned} di21(2m22n)<21[3n(n[2n])]<21(2n+[2n])
稍微讨论一下 n n n 是否为偶数。

  • 如果 2 ∣ n 2\mid n 2n d i d_i di n 2 n\over 2 2n 个,且 ∑ d i < n \sum d_i<n di<n
  • 如果 2 ∤ n 2\nmid n 2n d i d_i di n + 1 2 n+1\over 2 2n+1 个,且 ∑ d i < n + 1 2 < n + 1 \sum d_i<n+\frac{1}{2}<n+1 di<n+21<n+1

如果对比一下,就会发现问题已经变为了这个:

  • k k k 个正整数 d 1 , d 2 , d 3 , … , d k d_1,d_2,d_3,\dots,d_k d1,d2,d3,,dk 满足 ∑ d i < 2 k \sum d_i<2k di<2k ∑ d i \sum d_i di 为偶数,求系数 c i ∈ { − 1 , 1 } c_i\in\{-1,1\} ci{1,1} 使得 ∑ d i c i = 0 \sum d_ic_i=0 dici=0

因为 2 ⌈ n 2 ⌉ ⩾ n ⩾ ∑ d i 2\lceil{n\over 2}\rceil\geqslant n\geqslant\sum d_i 22nndi 嘛。这时候就可以采用 任意方案 了,比如最大值减去次大值。原因是:

  1. 若最大值 = 1 =1 =1,说明所有数都是 1 1 1,而 ∑ d i \sum d_i di 为偶数,故一半 c i = − 1 c_i=-1 ci=1 另一半 c i = 1 c_i=1 ci=1 就行。
  2. 若最大值不为 1 1 1,那么 k ≠ 1 k\ne 1 k=1,否则不符合条件。所以存在次大值。相减后,显然 ∑ d i \sum d_i di 会减小 ( ( ( 次大值 × 2 \times 2 ×2 ) ) ) 这么多。由于次大值 ⩾ 1 \geqslant 1 1 所以 ∑ d i \sum d_i di 至少减小 2 2 2,而数量至少减小 1 1 1,所以仍然满足条件。

此时审视我们的方案:最大减次大,并且将新的结果丢入堆。显然第 i i i 次作差时,减去的数不会小于原序列的第 2 i 2i 2i 大,因为我们新的结果如果用来当减数(或被减数)只会让减数更大。

于是,简单的 “最大值减次大值” 完全正确,并且合理的不得了!

最难的部分讲完了。最简单的部分是优化复杂度。直接用堆模拟是 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 的;利用桶排,就可以做到 O ( n ) \mathcal O(n) O(n) 了。

代码

输出格式要求还挺烦的……竟然要输出 c i c_i ci 才行……

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cctype>
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 int_;
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;
}
void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}

const int MAXN = 1000005;
/// sign of every value, ordered as a tree
/// with left-son positive and right-son negative
int son[MAXN<<1][2];
int a[MAXN<<1]; ///< value of every node ON TREE
void extractAnswer(int ans[],int x,int now){
	while(son[x][0]){
		extractAnswer(ans,son[x][1],-now);
		x = son[x][0]; // keep the sign
	}
	ans[x] = now; // leaf
}

struct Edge{
	int to, nxt;
	Edge() = default;
	Edge(int __to,int __nxt):to(__to),nxt(__nxt){ }
};
Edge e[MAXN<<1];
int head[MAXN], cntEdge;
inline void push_back(int x,int y){
	e[cntEdge] = Edge(y,head[x]);
	head[x] = cntEdge ++;
}

int ans[MAXN]; ///< real answer
int main(){
	int n = readint(), m = readint();
	memset(head,-1,(m+1)<<2);
	rep(i,1,n) push_back(a[i] = readint(),i);
	int tot = n; ///< giving index
	for(int i=m,lst=0; i; --i)
		for(int j=head[i]; ~j; j=e[j].nxt)
			if(lst == 0) lst = e[j].to;
			else{
				son[++ tot][0] = lst;
				son[tot][1] = e[j].to;  // this one
				a[tot] = a[lst]-i, lst = 0;
				if(a[tot] >= i) lst = tot; // still maximum
				else push_back(a[tot],tot);
			}
	for(int j=head[0]; ~j; j=e[j].nxt)
		extractAnswer(ans,e[j].to,1);
	puts("NP-Hard solved");
	rep(i,1,n){
		if(ans[i] == -1) putchar('-');
		putchar('1'); putchar(' ');
	}
	putchar('\n');
	return 0;
}

后记

我们解出了此题。我们走向了前人走过的路。我们向着普林斯普更近了一步。

我们对天空高呼:“普林斯普,为我祝福!”

只听见普林斯普的回声:“尔看榜首,吾于此处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值