Live2D

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

  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!

posted @ 2021-11-16 12:49  Rainybunny  阅读(1606)  评论(4编辑  收藏  举报