world.construct(me);
标题化用自 《world.execute(me);》。
0 引言
0.1 所谓构造题
构造题这种题型,不同于常见的计数或最优化问题,它一般要求选手给出任意一组符合一定要求的答案,即使冠以“最优的答案”之名,往往求得“最优值”比较轻易,构造题的难点集中于方案构造。而正如 OI Wiki 所介绍的:
构造题一个很显著的特点就是高自由度,也就是说一道题的构造方式可能有很多种,但是会有一种较为简单的构造方式满足题意。看起来是放宽了要求,让题目变的简单了,但很多时候,正是这种高自由度导致题目没有明确思路而无从下手。
构造题普遍的高思维难度以及老少咸宜(?)的代码实现难度,使得它在竞赛中的考察越来越频繁(NOIP 2020 T3, CSP-S 2021 T3, 所以下一道是……)。因此,雨兔 秉承直面恐惧的精神, 为大家整理归纳了一些构造题与其思想精髓,希望能帮助大家切掉 T3。(开始押题.jpg
0.2 重点是动机 (motivation)
如 Tiw 所言,构造题的思考方式和传统 OI 题的思考方式并没有很大隔阂。只不过,构造题将重心放在了思维而非算法优化,这导致思考过程中找不到“为靠向某某算法进行转化”的步骤,也导致看了题解后觉得“这条思考链并不长,而我想不到”继而自闭的后果,这很正常。面对构造题,应当做好抛弃前人封装好的所谓“算法”的觉悟,自己走出思考链,哪怕并不长,步步皆思维。正如你被要求造出高效维护区间的数据结构——在你学线段树之前。
而在写一道构造题的题解时,我觉得重点是动机——具体的构造方法是谜底,只有让人拍案叫绝的功能,但真正领悟一道题的价值,需要学习的是从谜面或思考,或打表,或猜测……在人类智慧的努力下得到谜底的过程。也希望大家写构造题题解的时候能够注意,读者真正想要的,是“为什么这么想”。
扯了那么多,我们开始叭。为了让例题发挥最大功效,我会先放题和具体解法,最后给出一个章节的总结。
1 实践出真知
1.1 「CSP-S 2021」「洛谷 P7915」回文
1.1.1 题目大意
Link.
1.1.2 解题过程
——手玩是正道。
读完题,抓住重点:每个数恰好出现两次 \(\Rightarrow\) 第一次操作数字 \(x\) 的时刻决定了第二次操作数字 \(x\) 的时刻。另一方面,也应该意识到本题设的难点:双端操作。
Motivation: 我想要给双端操作加上限制,例如……固定一个点,双端队列就会变成栈。
“固定一个点”?固定最后一次操作的点是最牢靠的……等等,根据上文的结论,固定最后一次操作的点不就是固定第一次操作的点吗?第一次能操作的点不就两个吗?!枚举一下,问题就解决了呀。
“我没这个想法?”别担心,你但凡手玩任何一组样例,哪怕只玩第一步,都能得到思路。
1.2 「ARC 110F」Esoswap
1.2.1 题目大意
Link.
1.2.2 解题过程
——样例是题解。
样例解释:
First, announce \(i=6\). We swap \(P_6(=5)\) and \(P_{(6+5)\bmod8}=P_3(=6)\), turning \(P\) into \(7,1,2,5,4,0,6,3\).
Then, announce \(i=0\). We swap \(P_0(=7)\) and \(P_{(0+7)\bmod8}=P_7(=3)\), turning \(P\) into \(3,1,2,5,4,0,6,7\).
Then, announce \(i=3\). We swap \(P_3(=5)\) and \(P_{(3+5)\bmod8}=P_0(=3)\), turning \(P\) into \(5,1,2,3,4,0,6,7\).
Finally, announce \(i=0\). We swap \(P_0(=5)\) and \(P_{(0+5)\bmod8}=P_5(=0)\), turning \(P\) into \(0,1,2,3,4,5,6,7\).
一方面,样例具有迷惑性,且构造题一般没有大样例,而对于小样例,出题人很可能在标算输出的解法上进行手动调整;另一方面,相信懒惰的出题人给出的方案一定程度上相似于标算解法。所以——
Motivation: 我想用“主观感知”调整并阐释出样例方案的遵循的模式。
观察本题样例解释,发现三次 \(p_i=5\),一次 \(p_i=7\)。考虑到样例的迷惑性,我们尝试让对 \(p_i=5\) 的操作挨在一起。交换第一步和第二步,发现操作序列仍合法。
接下来,我们强行解释该操作序列的内在逻辑:
- 希望 \(p_7=7\),反复操作 \(p_i=7\) 直到 \(p_7=7\);
- 希望 \(p_7=7\land p_6=6\),反复操作 \(p_i=6\)(样例中不需要操作),直到不满足 \(p_7=7\) 回到第一步,或满足 \(p_6=6\);
- 希望 \(p_5=5\land p_6=6\land p_7=7\),操作同上。
- ……
综上,交换策略为
- 选择不满足 \(p_i=i\) 的最大的 \(p_i\) 进行交换直到序列升序。
戏剧性的是,这波分析猛如虎,得到的并不是题解做法,也并没有证明构造的复杂度,但是我确实以这种构造方法通过了本题。这其实也反应了构造题给选手带来的机会——出题人难以想象到所有非正解做法并造出对应的 hack 数据,也就是说,扯淡的思路甚至更有可能得大分。
1.3 「多校联训」子集
1.3.1 题目大意
题意简述
将 $1\sim n$ 划分为 $k$ 组,每组大小相等且数字和相等。多测,$\sum n\le10^6$。
1.3.2 解题过程
——暴搜是艺术。
\(2\mid\frac{n}{k}\) 的情况显然,不断取最大最小值匹配即可。而就算 \(2\nmid\frac{n}{k}\),我们也不想浪费那么简单的构造方法——
Motivation: 我想尽可能快地将问题化归为 \(2\mid\frac{n}{k}\) 的情况。
联系样例 \(n=9,k=3\) 有解,我们尝试用“最快”的方法化归——将 \(1\sim 3k\) 分给集合,每个集合分 \(3\) 个,使得每个集合等和。这个……手玩有点困难啊。
Motivation: 我手玩不动。
写一个暴搜,大概是依次枚举 \(1\sim 3k\),再枚举数字填入集合 \(1\sim k\)。我记得我写出来搜到的第一组 \(k=5\) 时的解是:
1 ? ?
2 ? ?
3 ? ?
4 5 ?
? ? ?
这直接给我整不会了啊,长得那么丑(我甚至记不得具体长相),怎么找规律?注意到第一列 1 2 3 4
都齐了,那把 \(5\) 放第一列应该也有解?
Motivation: “字典序最小”的解并不优美,我想人为给它加上限制。
这样写了之后,第一列是 1 2 3 4 5
,也有一堆解,但其中字典序最小的解的后两列还是乱麻。这个时候,我随便翻了翻所有的解,看到一个:
1 9 14
2 10 12
3 6 15
4 7 13
5 8 11
这个第二列就有规律了:从中间位置开始,6 7 ...
,接着再从最顶上开始,9 10 ...
,第三列自然可以通过总和减出来。再依次规律验证一下 \(k=7\) 的情况,发现同样适用,就结束啦。
1.4 小结
为什么把“实践”放在第一节讲?当然是因为
劳动是人类的本质活动。 ——马克思
构造题难以下手,那就应当尝试从简单下手,如上文中对样例、样例解释和暴搜的灵活应用,虽然并不是严谨的“题解”,但应用在赛场上,不仅能帮助我们稳定心态,高效思考,也能在想不出正解时及时止损。
2 拼盘得正解
2.1 「CF 1450C2」Errich-Tac-Toe (Hard Version)
2.1.1 题目大意
Link.
2.1.2 解题过程
——别忘记小奥。
首先,就个人经验来说,不要妄想用调整法在棋盘上直接搞,如果你过了当我没说。(
Motivation: 三连棋我并不熟悉,但如果是“二连棋”……这是二分图?
想一想若是二连棋,我们可以把棋盘交替黑白染色,若令所有黑格只能是 X
,所有白格只能为 O
,那么必然平局;若令所有黑格只能是 O
,所有白格只能是 X
,也必然平局。但是,这两个方案都完全不能保证操作次数呢……
Motivation: 我想利用这两个并不一定正确的方案。
挣扎一下?如果一种方案不行,就采用另一种……等等,两种方案操作次数之和为 \(k\),根据鸽巢原理,必然有一种操作次数 \(\le\lfloor\frac{k}{2}\rfloor\)!
同理地,对于三连棋,我们直接三染色——令 \((i,j)\) 的颜色为 \((i+j)\bmod 3\),构造三种方案使得操作之和恰为 \(k\),就必然有一种操作次数 \(\le\lfloor\frac{k}{3}\rfloor\)。
2.2 「CF 1364D」Ehab's Last Corollary
2.2.1 题目大意
Link.
2.2.2 解题过程
——NPC Solver.
Motivation: 这类题想必大家见过了,常见的想法是:以一个问题无解为条件,解决另一个问题。
首先,如果无环,直接树上二染色选其中一个集合;否则,考虑极小(注意不必要是最小)的环的大小 \(s\):
若 \(s\le k\),回答问题二。
否则 \(s\ge k+1\),由于 \(s\) 是极小的环,所以环内没有弦,那么隔一个选一个,能选出 \(\lfloor\frac{k+1}{2}\rfloor\) 个,也即是 \(\lceil\frac{k}{2}\rceil\) 个点。
两个不保证正确性的解法构成互补关系,让程序自己挑一个舒服的来做叭。
2.3 小结
从鸽巢原理到 NPC 二选一(虽然 2.2 的第二问似乎是 P 的),我们应当把一些“显然的错解”记录下来,想一想它们的适用条件,说不定一个拼盘就拼出了全集。
3 博弈出题人
3.1 「Gym 102900B」Mine Sweeper II
3.1.1 题目大意
Link.
3.1.2 解题过程
——抓隐含条件。
请问你会算一个局面下所有格子数值的和吗?——会呀。
既然如此……出题人告诉我 \(B\) 干嘛?!
Motivation: 出题人告诉了我与问题不直接相关的 \(B\),那我就用 \(B\) 作为条件来构造 \(A\)。
同时,发现把一个局面的雷和空格反转,数值和不变(每对雷-空格的关系仍存在),那么 \(A\) 就能变成 \(B\) 和 \(\lnot B\) 中的一个。联系 2.1,两个方案反转总数是 \(nm\),根据鸽巢原理,必然存在一解。
3.2 「CF 1205D」Almost All
3.2.1 题目大意
Link.
3.2.2 解题过程
——猜系数由来。
你一看这个 \(\lfloor\frac{2n^2}{9}\rfloor\),就该想到 \(\frac{2}{9}=\frac{1}{3}\cdot\frac{2}{3}\)。 ——Tiw
出题人在这类题目里留下了奇怪的系数,看着很毒瘤,但请相信,那是出题人给你留下的提示。
希望不会有出题人帮你把 2/9 放松到 11/53 之类的玩意儿。
Motivation: 我想凑出 \(\frac{1}{3}\cdot\frac{2}{3}\) 的情景。
直接数字观察太科幻了,想一想这个乘法,很可能是乘法原理,也就是说……把树划分为两个部分,仅考虑两部分间的路径?
划分本身并不困难,但理应做到越平均越好,注意 \(\frac{2}{9}\) 应是下界。
Motivation: 子树大小平均 \(\Rightarrow\) 重心。
若能在重心上,我们能够将一些子树划为重心所在连通块作为第一部分,另一些子树一起作为第二部分,两部分有一个公共“顶点”重心,就能用乘法原理了。
事实上,这是可行的。不断合并最小的两棵子树即可。最“不平均”的情况即有三棵大小相等的子树,最终得到 \(\frac{1}{3}\) 和 \(\frac{2}{3}\)。
另一方面,我们还需要实现“乘法原理”。根据前文的铺垫,乘法中的“单个物品”是结点到重心的的距离。也就是说,设 \(A\) 是第一部分中结点到重心的距离集合,\(B\) 是第二部分中结点到重心的距离集合,我们希望 \(A+B=\{a+b\mid a\in A,b\in B\}=\{1,2,\dots,|A|\cdot|B|\}\)。
这是……进制?
不妨设 \(|A|\le|B|\),则令 \(A=\{1,\dots,|A|\}\),\(B=\{(|A|+1),2(|A|+1),\dots,|B|(|A|+1)\}\),不难发现我们达成了目标。
此时,问题已然转化为:为边赋权,使得每个结点到根的距离构成某集合 \(C\)。这个问题比较简单,把 \(C\) 中最小的值赋在根的某一条邻接边,以此分为两个子树归纳构造即可。
如果你去看官方题解,你会发现这里给出的解题过程几乎是反过来的。这个例子放在这里的作用是强调系数观察的重要性。实际解题过程中,思路因人而异,正推、逆推或者双向搜索各有优劣,看自己的习惯灵活使用吧。
3.3 小结
构造题难在“自由”,那么出题人给出的复杂限制不失为一种提示与帮助。从条件入手,从限制系数入手,结合问题背景合理联想想象。可以说,做构造题的另一种思路便是:猜猜出题人怎么做。
4 模块建大楼
4.1 「CF 1368C」Even Picture
4.1.1 题目大意
Link.
希望你解决 OneInDark 的加强题目:限制 \(k\le 3n\)。不过 Rainybunny 更希望你做到更优。
4.1.2 解题过程
——局部到整体。
那什么,确实很难不被推翻。(
Motivation: 我想构造一个贡献率高的结构。
什么贡献率高嘛?一坨点呐。
..#..
.###.
#####
.###.
..#..
(如果你构造的是正放的正方形,也会为了合法而调整成这种形状。)可惜的是,我们还是难以把这一坨弄合法。稍微变形一下?
..##.. ...##...
.####. ..####..
###### => ########
.####. #.####.#
..##.. #..##..#
#......#
########
首先,局部层面,这个模块自身是优秀的;其次,这个模块是可通过简单调整合法的;更有趣的,整体上说,这个模块可连续使用:
..##.....##..
.####...####.
- - - ############# - - -
.####...####.
..##.....##..
什么意思呢?有点像倍增:我们每次构造一大坨,使得其贡献恰好不超过 \(n\),然后将 \(n\) 减去贡献,继续构造。可以发现,这种构造方法所需的 #
的个数为 \(n+\mathcal O(\sqrt n)\),当 \(n=300\) 时仅需 \(442\) 个 #
。
当然,构造方法多种多样,希望得到更好的做法呢。
4.2 「OurOJ #46544」漏斗计算
4.2.1 题目大意
题意简述
定义一个运算结点 \(u\) 有两个属性:当前容量 \(x_u\)、最大容量 \(V_u\)。提供以下单元操作:
I
读入一个整数 \(x\),令新结点 \(u=(x,x)\)。F u
装满 \(u\) 结点,即令 \(x_u=V_u\)。E u
清空 \(u\) 结点,即令 \(x_u=0\)。C s
令新结点 \(u=(0,s)\)。M u
令新结点 \(v=(0,x_u)\)。T u v
不断令 \(x_u\leftarrow x_u-1,x_v\leftarrow x_v-1\) 直到 \(x_u=0\) 或 \(x_v=V_v\)。
构造不超过 \(10^4\) 次操作的一个运算方法,输入 \(a,b\),输出 \(ab\bmod 2^{18}\)。
\(0\le a,b\le 10^5\)。
4.2.2 解题过程
——模块化编程。
先从条件入手,发现给的运算实在垃圾的离谱,而且只有一个二元运算 \(T(u,v)\)。
Motivation: 只有一个二元运算,所以我想用且仅能用它来实现基本的“逻辑判断”功能。
再从问题考虑,不难想到用龟速成求 \(ab\),继而取模可以化简为“若某值大于 \(262144=2^{18}\),则令其减去 \(2^{18}\)。那么我们至少需要实现这些模块:
加法器:输入结点 \(u,v\),输出 \(w\),满足 \(x_w=x_u+x_v\)。
逻辑减法器:输入结点 \(u,v\),输出 \(w\),若 \(x_u\ge x_v\),\(x_w=x_u-x_v\);否则 \(x_w=x_u\)。
注意,模块应能够独立完成相应功能,并且足够简洁,切忌复杂化问题本身。用模块封装功能,本质上就是理清思维的过程。由于本题细节较复杂,下文讲解会佐以代码片段。
加法器比较方便:新建一个大容量点,把两个加数复制一份倒进去就好。先实现一个复制当前容量器:
inline int copyNum( const int u ) {
printf( "M %d\n", u ), ++node;
printf( "F %d\n", node );
return node;
}
再实现加法器:
inline int add( const int u, const int v ) {
printf( "C 1000000000\n" ); int res = ++node;
printf( "T %d %d\n", copyNum( u ), res );
printf( "T %d %d\n", copyNum( v ), res );
return res;
}
逻辑减法?先要判断大小关系。而 \(T(u,v)\) 之后 \(x'_u=\max\{x_u-x_v,0\}\),我们只需要判断一个结点的当前容量是否为 \(0\)。好消息是,我们能够实现逻辑非器:
inline int logicNot( const int u ) {
printf( "M %d\n", u ), ++node;
puts( "C 1" ), ++node;
printf( "F %d\n", node );
printf( "T %d %d\n", node, node - 1 );
return node;
}
内部逻辑比较易懂就不讲啦。在此基础上,实现普通减法器和逻辑减法器:
inline int sub( const int u, const int v ) {
printf( "M %d\n", v ); int tmp = ++node;
printf( "T %d %d\n", copyNum( u ), tmp );
return node;
}
inline PII logicSub( int u, int v ) {
int f = logicNot( sub( u = add( u, 1 ), v ) );
rep ( i, 1, 18 ) f = add( f, f );
printf( "M %d\n", f ), f = ++node;
printf( "T %d %d\n", v = copyNum( v ), f );
return { u = sub( sub( u, v ), 1 ), f };
}
逻辑减法器返回的 first
即减法结果,second
用于求解时重复利用。
底层方法实现之后,剩下的工作就简单了:输入 \(a,b\),计算 \(2a,4a,\cdots,2^{16}a\),枚举 \(b\) 是否大于等于 \(2^{16}\dots2^0\),若是,则减去,答案加上对应的权。都能用以逻辑非为基础的模块实现。
操作次数复杂度为 \(\mathcal O(\log^2 V)\)(倍增以及内部的逻辑减法),我实现的常数较大,不过封装成模块很易懂就是了。(
完整代码见 我的博客 嗷。
接下来建议挑战瓶子国和跳蚤国。(bushi
4.3 小结
正如上文所说,用模块封装功能,本质上就是理清思维的过程。从 4.2 这种真正意义上的功能封装到 4.1 所展示的“能独立实现功能,能协同组合运用”的“广义封装”,包括一些序列排序问题将给定操作组合成新的操作这种“步骤封装”,都有让问题“焕然一新”的作用。
5 生活在树上
5.1 「CF 1586E」Moment of Bloom
5.1.1 题目大意
Link.
5.1.2 解题过程
——手动加限制。
先考虑无解条件:若存在某个点 \(u\) 是奇数个询问路径的端点则无解。因为此时 \(u\) 的邻接边的总覆盖次数为奇数,与要求矛盾。
Motivation: 图上问题太难处理了,我想把它变成树。
注意这里的细节:我们先(在原问题上)判无解,再放到树上处理。因为“某棵树上有解”是“图上有解”的充分不必要条件,放到树上后尽可能保证必然有解,解法才算严谨。
变成树,那随便取一棵生成树。对于每个询问直接覆盖树上路径,结束了。在不满足上文无解条件时必然有解。
证明也比较简单:以子树角度考虑,每次把子树内向上的覆盖次数当成从父亲出发的询问,再删除子树。这能保持无解条件恒不成立。
建生成树,就像是针对图上构造题的二向箔。(
5.2 「IOI 2019」「洛谷 P5811」景点划分
5.2.1 题目大意
Link.
5.2.2 解题过程
——树上特殊点 & 性质要深挖。
Motivation: 同上,图上下不了手啊,建 DFS 树(DFS 树额外拥有“非树边均为返祖边”的性质)。记得需要证明树上无解等价于图上无解。
不妨设 \(a\le b\le c\),显然我们让 \(A,B\) 分别连通即可。而在树上考虑,有点像 3.2,无非是把树切成两部分,让 \(A,B\) 分别能被其中一部分容纳。设切出来的一棵子树大小为 \(s\),则 \(s\in[a,n-b]\cup[b,n-a]\)。
考虑构造?不。\(a,b,c\) 算数上的性质还没有被发现。由于 \(a\le b\le c,a+b+c=n\),所以 \(a\le\frac{n}{3},b\le\frac{n}{2}\),继而 \(n-b\ge b\),所以 \(s\) 只需保证有 \(s\in [a,n-a]\)。
先把能够得到答案的情况剔除于思考进程:以 \(r\) 任意结点为根的 DFS 树中,若存在子树大小 \(\in[a,n-a]\) 则可以构造,接下来,则把不存在子树大小 \(\in[a,n-a]\) 当成新条件使用。
接下来猜一猜 motivation?
Motivation: 子树大小限制 \(\Rightarrow\) 重心。
从 3.2 的“平均”到此处“限制”,可以发现所谓 motivation 可能不是“说出来就很有道理”,不然和推导没两样。正如某著名数学老师「硫氢根」所说,对已知知识要敏感,学会将性质“勾连”,“合理”联想。当然树上的重心啊直径啊你都拿出来试试也行。
设重心为 \(g\),由上文讨论,对于 \(g\) 的孩子 \(u\),应有 \(s_u\not\in[a,n-a]\Rightarrow s_u< a\)。同理,\(s_g\not\in[a,n-a]\Rightarrow s_g> n-a\),即若以 \(g\) 为根,\(g\) 原来父亲所在子树的大小也 \(< a\)。
可见,不管我们怎么调整,这棵 DFS 很难再产生解了。怎么办?想起被遗忘的返祖边了吗?我们得把它们加回去。
为利用返祖性质,还是保持 \(r\) 为根不变,且仍在 \(g\) 处考虑,返祖边的功能仅仅是:将 \(g\) 的一棵子树(或其子树的子树等,但这里用不到)丢到 \(g\) 的祖先(不包括本身 \(g\))上。令 \(t\) 为以 \(g\) 为根时父亲的子树大小,我们可以做到:令 \(t\leftarrow t+s_u,s_g\leftarrow s_g-s_u\),其中 \(u\) 是 \(g\) 的某个孩子,并且 \(u\) 子树内拥有指向 \(g\) 祖先的返祖边。
到此,树上的分析差不多了,问题抽象成:有两个数 \(x,y\) 和一个集合 \(\{z_k\}\),你可以时 \(y\) 减去一个子集和,\(x\) 加上同一个子集和,问能否使 \(y\in[a,n-a]\)。
显然,由于初始有 \(y>n-a\),若 \(\{z_k\}\) 中所有数的和都无法让 \(y\) 减小到不超过 \(n-a\) 的值,则无解;否则,由于区间 \([a,n-a]\) 的长度为 \(n-2a\ge\frac{n}{3}\),而每个 \(z_i\in[1,a)<\frac{n}{3}\),所以步长足够小,我们不断令 \(y\) 减去任意一个 \(z_i\),必然存在一个时刻 \(y\) 落入 \([a,n-a]\),此时我们得到了解。
另一方面,我们还需要 \(g\) 处无解等价于图上无解,而考虑 \(g\) 的过程中实际上用到了图上所有边,所以仅需讨论其余结点做类似于 \(g\) 的操作也无解,这里省略不提 ,留作习题。
5.3 「CF 1214H」Tiles Placement
5.3.1 题目大意
Link.
5.3.2 解题过程
——要素要察觉。
首先呢,只有长度为 \(k\) 的路径才可能导致无解嘛。
所以呢,这个 motivation 跟上一道挺对仗的:
Motivation: 路径长度限制 \(\Rightarrow\) 直径。
注意到颜色没有本质区别,不妨设直径 \((x,\dots,y)\) 按照 \(1~~2~~\cdots~~k~~1~~2~~\cdots\) 的周期染,此时考虑一条附在直径上结点 \(u\) 的链,链头是 \(v\)。不妨设 \(c_u=2\),那么如果从左到右看,\(c_v\) 必须是 \(3\);如果从右到左看,\(c_v\) 必须是 \(1\),除 \(k=2\) 的情况外,这两个要求是矛盾的!
因此可以发现,当 \(k\not=2\) 时,对于此类形式的链 \(P_v\),必须满足直径两端至少有一个结点,它走到这条链的尾端,长度仍 \(< k\),这样才能避免矛盾。也即是,\(\min\{P_v+P(u,x)|,P_v+P(u,y)\}< k\)。若所有链都满足条件,在以到直径较远一端的颜色为标准继续重复周期染色即可。
5.4 小结
我们了解树远胜于图,本小节则强调了对树上要素的“联想”。DFS 树、重心、直径……人为限制构造的条件,从关键的点、路径入手考虑,反而是对问题的简化。
6 调整化腐朽
正确断句:调整/化腐朽。(OneInDark 是魔鬼!
6.1 「CF 141E」Clearing Up
6.1.1 题目大意
Link.
注意题面中对“生成树”的定义不严谨,树中不能有重边或自环。
6.1.2 解题过程
——边界要活用。
少有的我能一眼秒的构造题,泪目。
Motivation: 先找找合法条件吧。
一个显然的条件是,若能用 S
边(或 M
边)就用,仍无法让树中含有至少 \(\frac{n-1}{2}\) 条 S
边(或 M
边)则无解。
然后呢,以对 S
边的判断为例,若判断为可能有解,那么我们会得到一棵 S
边多于 M
边的生成树。能否以此构造解呢?
Motivation: 考虑到树边的可替换性,所以我想用调整法,不断加入 M
边直到树合法。
正确性证明容易,这里不提。
6.2 「CF 1396E」Distance Matching
6.2.1 题目大意
Link.
6.2.2 解题过程
Motivation: 先想想解的上下界?
从每条边的角度考虑,设边 \((u,v)\) 删去后得到的两棵树大小为 \(s_u,s_v\),那么这条边最多被覆盖 \(\min\{s_u,s_v\}\) 次,最少被覆盖 \([2\nmid s_u]\) 次。据此我们可以得到答案的上界 \(U\) 和下界 \(D\)。注意求 \(U\) 时直接以重心为根就能去掉 \(\min\)。
其实此时足够我们发现一个性质:\(k\) 存在解还需保证 \(k\equiv U\equiv D\pmod2\)。怎么证明?每次研究任意调整对答案奇偶性的影响。
Motivation: 我想沿用证明奇偶性结论的思想,通过调整构造答案。
以对 \(U\) 的调整为例:每次可选最大子树内的两个点 \(u,v\),记它们的 LCA 为 \(w\),深度为 \(d_w\),则将它们配对,答案减少 \(2d_w\)。注意点数为偶数保证了最大子树的大小 \(-2\) 时重心可以保持不变,所以修改后必然能够找到对应的解。由于 \(d\) 的是连续的,所以必然能够找到合适的调整方法。需要用 STL 精细实现,但这不是主要矛盾所以略过。(
6.3 小结
找答案上下界,再从其中一个向所求答案调整。这种思路让我们着眼于每次操作的影响,更容易发现操作本身的性质。
7 归纳为神奇
正确断句:归纳/为神奇。(来自 OneInDark 的建议。
7.1 「CF 1470D」Strange Housing
7.1.1 题目大意
Link.
7.1.2 解题过程
——证明即构造。
Motivation: 我能否在证明解的存在性的同时构造方案?
若图不连通显然无解。下归纳证明图连通时有解,且对于任意结点 \(u\),存在一组解,使得 \(u\) 结点住了老师。
首先,若 \(V=\varnothing\),显然有解。
接着,取任意一点 \(u\),令 \(u\) 住下老师。在 \(G\) 上删除与 \(u\) 邻接的所有点及其连边,归纳构造每个连通块,同时钦定连通块中某个与删去点曾经相连的点住下老师。可见归纳总能完成。
实现的时候可以直接按 DFS 序枚举结点,能住老师就住。这里的归纳只是为了写着更方便。(
7.2 「WF 2014」「洛谷 P6892」Baggage
7.2.1 题目大意
Link.
7.2.2 解题过程
——转化向边界。
Motivation: 样例的最小操作次数为 \(n\) \(\Rightarrow\) 最小操作次数为 \(n\)。(别笑,很常用的技巧。
注意,对于最小化问题,先猜或证最小值,知道了最小值才有构造的目的性。
对于 \(n\) 较小的情况,谨遵 1.4 中的教诲,我们能暴搜打表。对于大一点的情况,我们能否设计一个归纳?
Motivation: 我想归纳。(简单明了.jpg
具体而言,我们尝试把 _ _ B A B A ... B A
变成 A A A ... B B B ... _ _
。这里简明地给出 jiangly 论文里的 例子:
_ _ B A B A B A B A ...B A B A B A
A B B A B A B A B A ...B A B _ _ A
A B B A _ _ B A B A ...B A B B A A
对于第三行中 _ _ B A B A ... B A
进行递归:
=>
A B B A A A A ...B B B _ _ B B A A
A _ _ A A A A ...B B B B B B B A A
A A A A A A A ...B B B B B B B _ _
就行了。至于“如何得到这种归纳”,鄙人只能想到手玩一种方法。(
7.3 小结
联系 4.3,我们的归纳过程本质上是将目标的分段化处理。正如万能的 DP 一样,抓住问题的“子结构”,将远在天边的目标拉向眼前。
8 走出构造题
8.1 「OurOJ 46602」糖
8.1.1 题目大意
题意概述
数轴上依次有 $(n+1)$ 个关键点,第 $0$ 个点为原点,第 $i$ 个关键点的坐标是 $a_i$。你从 $0$ 出发,每走一单位吃掉一颗糖,每到达一个关键点,可以以 $b_i$ 的单价买糖,以 $s_i$ 的单价买糖,但最多携带 $C$ 颗糖。求到达 $n$ 号关键点时的最小花费(假设你初始的钱足够多)。$n\le2\times10^5$,保证有解且答案有限。
8.1.2 解题过程
——处处皆构造。
我不禁陷入沉思(?)……为什么一定要在构造题里构造?
Motivation: 算是经验之谈,我想找到这个买卖情景的等价转化。
正所谓凭空买,凭空卖,等价操作赚大钱(?)我们构造以下两个操作组合:
买入再以相同价格退掉 \(\Leftrightarrow\) 不买,所以每到一个关键点可以补满背包。
买入,篡改价格,退掉 \(\Leftrightarrow\) 低买高卖,所以我们只需要处理“买”和“退”两种操作。
这个时候贪心变得自然:背包里留下买得贵的糖。走到 \(i\) 时,维护当前背包内剩余的糖果集合 \(S\),并保持价格单调性。将背包内所有价格 \(<s_i\) 的糖果价格篡改为 \(s_i\)(卖);将背包内所有价格 \(>b_i\) 的糖果价格改为 \(b_i\)(重新买),并用当前 \(b_i\) 补满背包;最后吃掉下一步需要的糖果(挑便宜的吃咯)。
8.2 「OurOJ 28793」硬币游戏
8.2.1 题目大意
题意概述
你有 $n$ 组硬币,第 $i$ 组由上到下的价值依次是 $a_i,b_i,a_i$,只有取走上面的硬币才能取下面的。对于 $k=1,2,\dots,3n$,求总共取走恰好 $k$ 个硬币时的最大价值和。$n\le10^6$
8.2.2 解题过程
——一招解限制。
Motivation: 这个先后关系好难啊。
构造,硬币组 \((a,b,a)\) 等价于体积为 \(1\) 的硬币 \(a\) 和体积为 \(2\) 的硬币 \((a+b)\)。等价性显然。
接下来问题变得常规。两类体积的物品内显然选价值最大的;从选 \(k\) 体积的最优方案转移到选 \(k+1\) 体积的最优方案存在,且至多退掉一个体积为 \(1\) 的硬币。简单模拟即可。
8.3 小结
构造是一种思想。
正如你不会觉得四处乱窜的倍增算法很突兀,构造是可以无处不在的。有时候遇到题目的种种限制,不妨尝试构造,从等价题意轻松破题。
9 何为构造题
大家辛苦啦!讲题到此结束啦!我也写不动啦!
十八道花式构造下来,再思考“何为构造题”这个问题,来一个最后的交流总结吧。
9.1 构造出何物
我们构造出了什么?
是解吗?我认为那是结果,而非本质。
就我的观点,我们构造迈出的第一步,是构造限制。
我们构造出强于题目要求的限制,“自我约束”,让双端队列成了栈,让构造扫雷图成了合法性检查,让构造目标解成了构造子问题……既然“高自由度”是难点,“降低自由度”,就是我们做构造题最原始的 motivation。
很有趣吧,别的题里额外限制通向部分分,构造题里额外限制通向正解。
9.2 动机从何来
我不知道。就像 OneInDark 今天爆切 CF 3100 的构造后感慨,这个 motivation 太微弱了。动机这玩意儿多少和缘分挂钩。(
就多见题,多练题,也许吧。这种思维训练就想神经网络的 BP 一样。做一题,你有各种 motivations,在这题没用的,留一点“不好用”的感觉;在这题有用的,留一点“好用”的感觉;看题解才想到的,留一点“能用”的感觉。等训练次数够了,你试错的时间和精力消耗就少了,解题能力自然就高了。
其实我在一道道做本文的例题时,或多或少(基本上很多)地进行了规模不小的思路试错,看完题解再简单的构造题也能让人自闭。总而言之,不要怕构造题的毒打,多练,同时提炼总结每个细小的 motivation,不断积累,不断变强叭!顺便,本文也有一些没有涉及的版块,比如用欧拉路等图上模型将构造转化为图论算法问题,有空再补充(咕。
10 参考资料 & 致谢
「Q & A」不可食用的问答贴 by Tiw.
IOI2021 集训队论文 - 信息学竞赛中构造题的常用解题方法 by jiangly.
Tiw_Air_OAO 的博客中的构造专题 by Tiw.
感谢 Tiw 回答我在问答帖中的问题,我看见 Tiw 翻了包括 jiangly 论文在内的很多资料,真的好细心 www。
再次感谢 Tiw 向我推荐了那么多构造神题,本兔最喜欢她了 w(?
感谢 OneInDark 提供的长期精神支持。
为什么致谢?一个次要原因是算上文档末尾的保留空行后,markdown 源码恰好 \(712\) 行 awa!
world.construct(me);的更多相关文章
- Leetcode, construct binary tree from inorder and post order traversal
Sept. 13, 2015 Spent more than a few hours to work on the leetcode problem, and my favorite blogs ab ...
- [LeetCode] Construct Binary Tree from Inorder and Postorder Traversal 由中序和后序遍历建立二叉树
Given inorder and postorder traversal of a tree, construct the binary tree. Note: You may assume tha ...
- [LeetCode] Construct Binary Tree from Preorder and Inorder Traversal 由先序和中序遍历建立二叉树
Given preorder and inorder traversal of a tree, construct the binary tree. Note:You may assume that ...
- Leetcode Construct Binary Tree from Inorder and Postorder Traversal
Given inorder and postorder traversal of a tree, construct the binary tree. Note:You may assume that ...
- 【LeetCode OJ】Construct Binary Tree from Preorder and Inorder Traversal
Problem Link: https://oj.leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-trave ...
- LeetCode OJ 106. Construct Binary Tree from Inorder and Postorder Traversal
Given inorder and postorder traversal of a tree, construct the binary tree. Note:You may assume that ...
- Construct Binary Tree from Inorder and Postorder Traversal
Construct Binary Tree from Inorder and Postorder Traversal Given inorder and postorder traversal of ...
- Construct Binary Tree from Preorder and Inorder Traversal
Construct Binary Tree from Preorder and Inorder Traversal Given preorder and inorder traversal of a ...
- Reorder array to construct the minimum number
Construct minimum number by reordering a given non-negative integer array. Arrange them such that th ...
- Leetcode Construct Binary Tree from Preorder and Inorder Traversal
Given preorder and inorder traversal of a tree, construct the binary tree. Note:You may assume that ...
随机推荐
- Linux的六种查找命令
http://www.ruanyifeng.com/blog/2009/10/5_ways_to_search_for_files_using_the_terminal.html 1. find fi ...
- PPT变为gif效果
原文链接:https://www.toutiao.com/i6496812863263343117/ 首先我们点击"文件按钮" 在下拉列表中选择"另存为" 在& ...
- Servlet中分发器和重定向两兄弟
注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6513702111698485767/ 弄清这个两兄弟,我们还是从练习中去理解 先创建一个数据提交页面,注意路径 编 ...
- 2018HPU暑期集训第四次积分训练赛 K - 方框 题解(图形打印)
思路分析:题目已经明确透露了这道题的解法:就是画框.当 输入的边长 的话,就表示可以在内层继续嵌套一个方框.废话就不多说了,直接上代码吧! 代码如下: #include <iostream&g ...
- RHCSA 第八天
1.查询ip的几种方式: ip, ifconfig, nmcli,nmtui 2.nmcli命令使用: a.在ens160网卡上新建连接static_con,并配置静态ip b.在ens160网卡上新 ...
- 《剑指offer》面试题05. 替换空格
问题描述 请实现一个函数,把字符串 s 中的每个空格替换成"%20". 示例 1: 输入:s = "We are happy." 输出:"We%20a ...
- unity3d发布安卓出错plese set the package name
发布时报错 参考https://forum.unity.com/threads/where-is-package-name-setting.318839/ 参考https://answers.unit ...
- 新手如何入门linux,linux原来还可以这么学
前言 在这个只有cangls和小白两人的小房间中,展开了一次关于学习方法的讨论. 小白:cangls啊,我想请教一个问题,您是如何记住那么多linux命令的. cangls:我啊,别人都看我的小电影, ...
- Linux下的ssh、scala、spark配置
注:笔记旨在记录,配置方式每个人多少有点不同,但大同小异,以下是个人爱好的配置方式. 目录 一.配置jdk 二.配置ssh 三.配置hadoop 四.配置scala 五.配置spark 平台:win1 ...
- HttpServletRequest类介绍
HttpServletRequest类介绍 1,HttpServletRequest类作用: 每次只要有请求进入Tomcat服务器,Tomcat服务器就会把请求过来的HTTP协议信息解析好封装到Req ...