目录

  标题化用自 《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 题目大意

  Private link.

题意简述

  将 $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 题目大意

  Private Link.

题意简述

  定义一个运算结点 \(u\) 有两个属性:当前容量 \(x_u\)、最大容量 \(V_u\)。提供以下单元操作:

  1. I 读入一个整数 \(x\),令新结点 \(u=(x,x)\)。

  2. F u 装满 \(u\) 结点,即令 \(x_u=V_u\)。

  3. E u 清空 \(u\) 结点,即令 \(x_u=0\)。

  4. C s 令新结点 \(u=(0,s)\)。

  5. M u 令新结点 \(v=(0,x_u)\)。

  6. 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 题目大意

  Private link.

题意概述

  数轴上依次有 $(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 题目大意

  Private link.

题意概述

  你有 $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 参考资料 & 致谢

  为什么致谢?一个次要原因是算上文档末尾的保留空行后,markdown 源码恰好 \(712\) 行 awa!

world.construct(me);的更多相关文章

  1. 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 ...

  2. [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 ...

  3. [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 ...

  4. 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 ...

  5. 【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 ...

  6. 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 ...

  7. Construct Binary Tree from Inorder and Postorder Traversal

    Construct Binary Tree from Inorder and Postorder Traversal Given inorder and postorder traversal of ...

  8. Construct Binary Tree from Preorder and Inorder Traversal

    Construct Binary Tree from Preorder and Inorder Traversal Given preorder and inorder traversal of a ...

  9. Reorder array to construct the minimum number

    Construct minimum number by reordering a given non-negative integer array. Arrange them such that th ...

  10. 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 ...

随机推荐

  1. Linux的六种查找命令

    http://www.ruanyifeng.com/blog/2009/10/5_ways_to_search_for_files_using_the_terminal.html 1. find fi ...

  2. PPT变为gif效果

    原文链接:https://www.toutiao.com/i6496812863263343117/ 首先我们点击"文件按钮" 在下拉列表中选择"另存为" 在& ...

  3. Servlet中分发器和重定向两兄弟

    注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6513702111698485767/ 弄清这个两兄弟,我们还是从练习中去理解 先创建一个数据提交页面,注意路径 编 ...

  4. 2018HPU暑期集训第四次积分训练赛 K - 方框 题解(图形打印)

    思路分析:题目已经明确透露了这道题的解法:就是画框.当 输入的边长  的话,就表示可以在内层继续嵌套一个方框.废话就不多说了,直接上代码吧! 代码如下: #include <iostream&g ...

  5. RHCSA 第八天

    1.查询ip的几种方式: ip, ifconfig, nmcli,nmtui 2.nmcli命令使用: a.在ens160网卡上新建连接static_con,并配置静态ip b.在ens160网卡上新 ...

  6. 《剑指offer》面试题05. 替换空格

    问题描述 请实现一个函数,把字符串 s 中的每个空格替换成"%20". 示例 1: 输入:s = "We are happy." 输出:"We%20a ...

  7. unity3d发布安卓出错plese set the package name

    发布时报错 参考https://forum.unity.com/threads/where-is-package-name-setting.318839/ 参考https://answers.unit ...

  8. 新手如何入门linux,linux原来还可以这么学

    前言 在这个只有cangls和小白两人的小房间中,展开了一次关于学习方法的讨论. 小白:cangls啊,我想请教一个问题,您是如何记住那么多linux命令的. cangls:我啊,别人都看我的小电影, ...

  9. Linux下的ssh、scala、spark配置

    注:笔记旨在记录,配置方式每个人多少有点不同,但大同小异,以下是个人爱好的配置方式. 目录 一.配置jdk 二.配置ssh 三.配置hadoop 四.配置scala 五.配置spark 平台:win1 ...

  10. HttpServletRequest类介绍

    HttpServletRequest类介绍 1,HttpServletRequest类作用: 每次只要有请求进入Tomcat服务器,Tomcat服务器就会把请求过来的HTTP协议信息解析好封装到Req ...