题目
题目背景
为了重新见到普林斯普,
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
3⩽n⩽m⩽106,且题目描述中的限制条件都将得到满足。
思路
据出题人 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=a2i−a2i−1(i⩽2n),为了方便,先假定
2
∣
n
2\mid n
2∣n 吧。如果在数轴上考虑这个问题,你就会发现,一个
d
i
d_i
di 相当于一个线段的长度,而这
n
2
\frac{n}{2}
2n 个线段是不相交的(即使端点也不相交),所以中间一定会有
(
n
2
−
1
)
(\frac{n}{2}-1)
(2n−1) 个长度至少为
1
1
1 的空隙,而总长是
[
1
,
m
]
[1,m]
[1,m] 的长度
(
m
−
1
)
(m-1)
(m−1),所以
∑
d
i
⩽
m
−
⌊
n
2
⌋
\sum d_i\leqslant m-\left\lfloor\frac{n}{2}\right\rfloor
∑di⩽m−⌊2n⌋
为什么加了整除符号?是因为加入了
2
∤
n
2\nmid n
2∤n 的情形——可以假定
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
∑di⩽m−⌊2n⌋,也就是上面的式子。
继续 放缩,因为题目中
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}
∑di⩽21(2m−2⌊2n⌋)<21[3n−(n−[2∤n])]<21(2n+[2∤n])
稍微讨论一下
n
n
n 是否为偶数。
- 如果 2 ∣ n 2\mid n 2∣n 则 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 2∤n 则 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 2⌈2n⌉⩾n⩾∑di 嘛。这时候就可以采用 任意方案 了,比如最大值减去次大值。原因是:
- 若最大值 = 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 就行。
- 若最大值不为 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;
}
后记
我们解出了此题。我们走向了前人走过的路。我们向着普林斯普更近了一步。
我们对天空高呼:“普林斯普,为我祝福!”
只听见普林斯普的回声:“尔看榜首,吾于此处。”