新坑,构造,900~1300

注意到,每次 cf 遇到构造题,我都会死死锁住,导致最后不够时间写其他题。

于是打算学点较为自然的构造思路。

900 构造

说实话感觉 900 的构造更像傻软数学题,根本不是构造题,以下随便写几道。

1

https://codeforces.com/problemset/problem/1879/B
问:
\(n \times n\) 的网格,\((i,j)\) 位置上的权值是 \(a_i + b_j\)\(\{a\},\{b\}\) 会给出。要求放若干个棋子在网格中,满足:任意 \((i,j)\) 位置,或者横坐标 \(i\) 或者纵坐标 \(j\) 上存在棋子。询问最小的棋子所在位置的权值和。

思考:
直觉上这是个非攻击型直车问题,看了一眼样例后发现不是。因为非攻击型直车问题要求:任意 \((i,j)\) 位置,横坐标 \(i\) 且纵坐标 \(j\) 上都不存在两个棋子。

  • 很好,傻狗直觉,下次注意存在和不存在的关键字。

真正的直觉:“或者任意横坐标 \(i\) 或者任意纵坐标 \(j\) 上存在棋子”。问题独立。
分两类讨论:

  • 任意横坐标 \(i\) 存在棋子。纵坐标无要求,故选最小;
  • 任意纵坐标 \(j\) 存在棋子。横坐标无要求,故选最小。

两个分类的答案取 \(min\) 即原问题答案。

2

https://codeforces.com/problemset/problem/1869/A
问:
\(\{a\}\) 满足 \(|\{a\}| \geq 2\) 。构造不超过 \(8\) 次操作:选 \(1 \leq l < r |\{a\}|\) ,将 \(l \leq i \leq r\) 的所有 \(a_i\) 赋值为 \(\bigoplus_{i=l}^{r} a_i\) ,使最后 \(\{a\}=[0,0, \cdots, 0 ]\) 。问题表示,可以证明一定能做到。

思考:
基于一个数学竞赛出身的学生的基本素养,可以证明有限次能做到,比如一直选相邻两个不全为 \(0\) 的位置。这个证明不重要。

考虑如何 \(8\) 次内做到。我打算使用严格数学归纳法。
\(n=2\) ,可以在 \(\leq 2\) 次做到。三类情况是:

  • \(0\)
  • 存在 \(0\) 也存在 \(>0\)
  • \(>0\)

\(n \geq 2\) ,我们带着贪心去做归纳:
选最长覆盖段,覆盖所有 \(>0\) ,长为 \(l\)

  • \(2 \mid l\)\(\leq 2\) 次能做到。
  • \(2 \not \mid l\) ,且 \(\bigoplus a_i \neq 0\) ,我们把前 \(l - 1\) 个一次性扬了(除非是空,且 \(\leq 2\) 次)。
    • \(l\) 个和它相邻一个做 \(2\) 次,主要取决于哪个相邻的数存在(因为 \(n>1\) 故一定存在一个)。
  • 总次数 \(\leq 4\)
  • 显然 \(n=2\) 也满足。

于是我们只需要 \(\leq 4\) 次。当然其实有更紧的做法。


我怎么归纳了这么久?起码五分钟。
我怎么写了这么久?二十分钟。下标看错了,为啥啊。我状态不好的时候真不容易写出代码啊。打线下的时候记得调好状态啊服了
他们不是说这种题最好五分钟以内要 ac 吗。真对不起啊我多写点。


初见端倪,900 rating 的构造,乱搞就能五分钟按严格数学归纳做出来。

3

https://codeforces.com/problemset/problem/1860/A
问:
合法括号序列:我自己的定义是符合出栈入栈顺序,不看题目的傻逼定义了。
给一个括号序列 \(s\) (不一定合法)满足 \(|s| \geq 2\) ,找到一个 \(2|s|\) 长的合法括号序列 \(t\) 满足 \(s\) 不是 \(t\) 的子集。

思考:
我要用严格数学归纳法解决这个问题。

  • \(|s|=2\) 。合法括号序列有 "()()" 和 "(())" 。
    • "s=()" ,无解。
    • "s=)(" ,一个解是 "(())" 。
  • \(|s|=3\) 。合法括号序列有 "()()()" 、"(()())"、"()(())"、"(())()"、"((()))"。
    • "s=(()", 一个解是 "()()()" 。
    • "s=())" ,一个解是 "()()()" 。
    • "s=()(" ,一个解是 ""((()))"" 。
    • "s=)()" ,一个解是 ""((()))"" 。
  • \(|s|>3\)
    • 若存在子串 ")(" ,则 "((...))" 总是解。\(s\)\(t\) 无公共子串 ")(",则 \(s\) 不会是 \(t\) 的子串。
    • 否则是 "((...))",则 "()()...()" 总是解。显然 \(s \not \in t\)\(s\)\(t\) 无公共的 ")("。
    • \(|s|=3\) 也满足。

难道,傻逼 900 构造,只是:严格数学归纳一下、归纳时可能带点贪心,或者是弱智数学题?不做了。


4

https://codeforces.com/problemset/problem/1818/B
问:
\(n\) ,构造该大小的排列 \(\{a\}\) 满足:\(\forall l,r, 1 \leq l < r \leq n, (r-l+1) \not \mid \sum_{i=l}^{r} a_i\) 。或者说做不到。

思考:
我要用严格数学归纳解决这题。

  • \(n=1\) ,不存在 \(1 \leq l < r \leq n\)
  • \(n=2\) ,总有 \(2 \not \mid (a_1 + a_2)=3\)
    • \(2 \not \mid (a_l + a_{l+1})\) 当且仅当 \(a_l\)\(a_{l+1}\)\(2\) 不同余。
      • 故而下面的归纳可以尝试按剩余类讨论。
  • \(n=3\) ,总有 \(3 \mid (a_1 + a_2 + a_3)=6\)
    • 注意到 \(\sum_{i=1}^{n} i = (n+1)n/2\) ,当 \(2 \not \mid n\) ,则总有因子 \(n\) ,总搞不了。
      • 故而只有 \(n>1\)\(2 \mid n\) 时有机会搞。
      • 故而不能出现 \(1,2,3, \cdots\) 这种递增前缀。
  • 对于 \(2 \mid n\) ,现在我们对 \(\forall n, 2 \mid n\) 尝试一个自然的构造。
    • 按模 2 的剩余类构造,奇数位置元素不变,偶数位置元素递减。
      • 暴力拍一下发现不对。(拍的时候下标又错了,可恶,我的码力)
      • 再让奇数位置元素递减,偶数位置元素递减。拍一下对了。
        • 所以这个题不就是纯纯傻逼题

好,\(n=4\) 的时候,发现只有 \(3,4,1,2\) 是对的,然后不会做了,再往下去归纳也归纳不出。只知道两个必要条件

  • 排列应该是奇偶相邻的
  • \(2 \not \mid n\) 时没有解

然后随便按最自然的方式乱构造了一下,(从 \(n=4\) 出发,这时候比较自然的方式应该是奇数和偶数上的位置都递减)直接拍对了,证我又不会证。这不就纯傻逼题?


让我们看看题解。题解给出了另一种构造方法,是易控易证的。
首先两个必要条件是显然的。
然后只把奇偶直接换一下。就是这样构造 \(2,1,4,3,6,5, \cdots, n,n-1\)

  • 更本质地,从前缀和来看,这个方法等于每相邻 \(2\)\(swap\)
  • 从构造模型来看,这个方法是,拆分奇偶,尝试按先偶再奇并且正序地交替填入。
    这非常可控,因为对于任意 \(m\)
  • \(2 \mid m\) ,总有 \(f(m)=\sum_{i=1}^{m} a_i = m(m+1)/2\)
  • \(2 \not \mid m\) ,总有 \(f(m)=\sum_{i=1}^{m} a_i = m(m+1)/2+1\)
    于是取 \(f(r) - f(l-1) = (r-l+1)(r-l+2)/2\) 当且仅当 \(r,l-1\) 是同一剩余类,否则 \(f(r) - f(l) = (r-l+1)(r-l+2)/2 \pm 1\)
    \(r,l-1\) 是同一剩余类,则 \(2 \mid (r-l+1)\) ,且 \(2 \not \mid (r-l+2)\) ,所以我们没法搞出因子 \(r-l+1\)
    \(r,l-1\) 不是同一剩余类,则 \(2 \mid (r-l+2)\) ,我们无法从 \(pq \pm 1 \ s.t. \ p>q\) 中搞出因子 \(p\)

虽然这题是傻逼题,乱搞就能对,但从题解里还是:学是学到了,简单将奇偶位上的元素互换,对于前缀和来说是易控的。

再说一遍,分奇偶的前缀和是易控的。再说一遍,更朴素地自然顺序的前缀和换位置,也是易控的。


至于为什么我们乱搞能对?因为注意到我们的构造如果 \(reverse\) 一下,就是题解给出的构造。

实际上如果我喜欢尝试 \(reverse\) ,也能发现我的构造是易控的,从而易证。


收回我的所谓的 900 的构造全是傻逼题的话,构造题里还是很多东西能学啊……


码力很有问题怎么办…… MIT1 的 WF 队伍也有队员码力有问题,但是依然不妨碍他能做对 WF hard 题。

邓明扬老师和戴江齐老师的码力也有问题,但是他们持之以恒的码力训练后,也能达到一个相对强的程度。而且他们也依旧能凭借非顶级的码力进入国家集训队、国家候选队、国家队,最后拿到 IOI 金牌。

努力!


5

https://codeforces.com/problemset/problem/1806/B
我绰,我要坐起来打,题目名字是什么 Mex Master 。

问:

给一个长度为 \(n\)\(\{ a \}\) ,它的 \(score\)\(mex(a_1+a_2,a_2+a_3, \cdots, a_{n-1}+a_n)\) 。如果你能重排 \(\{ a \}\) ,找到它的最小 \(score\) ,注意你不需要构造 \(\{ a \}\)

思考:

\(mex\) 的简单题,肯定是先从小到大枚举一个值考虑。注意我们只需要考虑新数组 \(\{ b \}\) \(b_i \in [0,n]\) ,因为 \(mex\) 最坏是 \(n\) ,这时 \(\{ b \}\) 取遍 \([0,n-1]\)

再看一眼样例,注意我们要让 \(mex\) 最小。

  • \(0\) 如果一定能被构造,说明 \(0\) 的数量 \(\leq \lceil \frac{n}{2} \rceil\)
  • 否则 \(0\) 一定不是 \(mex\) ,不需要在意 \(0\) 的位置,只需要要考虑构造 \(1\) 。这意味着我们需要让 \(a_i + a_j > 1\)
    • 如果不存在 \(1\) 那很好,\(mex=1\)
    • 否则注意所有 \(1\) 需要不和任意一个 \(0\) 相邻, 分开放是没必要的,我们 \(1\) 都放一起,只需要存在两个 \(> 1\) 的数就可以把 \(1\)\(0\) 隔开。
  • 否则 \(1\) 不能被构造,这时候问题变得很可控,我们有 $> \lceil \frac{n}{2} \rceil $ 的 \(0\) ,且存在 \(1\) ,且不存在两个 \(>1\) 的数(**补充:这里是想的把 **
    \(1\) 插到 \(0\) 里面,显然这个做法是非常不优的,因为有更优的做法,把 \(1\) 放到边上),换句话说有 \(0 \sim 1\)\(> 1\) 的数,因为 \(0,1\) 一定不能被构造,他们的位置不重要。考虑多余的数:
    • 如果没有多余的数,那么 \(mex\) 最小时 \(0,1\) 不能被构造,即是 \(2\)
    • 否则只存在一个多余的数,那么看他是不是 \(2\) 。如果不是 \(2\)\(mex=2\) ;如果是 \(2\) ,当且仅当有两个 \(1\) ,能把 \(2\)\(0\) 隔开,使得 \(0,1\) 连续出现,\(mex=2\) ,否则 \(0,1,2\) 连续出现,\(mex=3\)

hack
有一个点期待是 \(1\) ,结果算成了 \(3\)

感觉我是个唐 b 了,注意把 \(1\) 放一起,只需要存在一个 \(>1\) 的数就可以把 \(1\)\(0\) 隔开,因为我们可以把 \(0\)\(1\) 放两边。为什么唐呢,因为我考虑的是把 \(1\) 插到 \(0\) 的里面,没有刻意描述这个操作,如果刻意描述了,会发现 \(1\) 放到 \(0\) 的另一边会更好。

正确思路:(按这个做法交的代码是对的)

  • \(0\) 如果一定能被构造,说明 \(0\) 的数量 \(\leq \lceil \frac{n}{2} \rceil\)
  • 否则 \(0\) 一定不是 \(mex\) ,不需要在意 \(0\) 的位置,只需要要考虑构造 \(1\) 。这意味着我们需要让 \(a_i + a_j > 1\)
    • 如果不存在 \(1\) 那很好,\(mex=1\)
    • 否则注意所有 \(1\) 需要不和任意一个 \(0\) 相邻, 分开放是没必要的,我们 \(1\) 都放一起,只需要存在 \(1\)\(> 1\) 的数就可以把 \(1\)\(0\) 隔开。这时候 \(0,1\) 在两边,中间插入一个数就好。
  • 否则 \(1\) 不能被构造,这时候问题变得很可控,我们有 $> \lceil \frac{n}{2} \rceil $ 的 \(0\) ,且存在 \(1\) ,且不存在 \(>1\) 的数,换句话说有 \(0\)\(> 1\) 的数,这时候 \(0,1\) 连续出现,\(mex=2\)

小讨论讨论不清晰就很致命,虽然这题简单,但是赛场上一时半会真不会知道哪里错了,就是平时不好好注意刻画细节。

实现的时候我们只要记一下 \(0,1\) 出现次数。这里放一下代码,很久没写过讨论了。

view
cin>>n;
int v[3]={0};
L(i,1,n){
	int x;cin>>x;
	if(x<=2)v[x]++;
}
if(v[0]<=(n+1)/2)ans=0;
else if(v[1]==0)ans=1;
else if(n-v[0]-v[1]>=1)ans=1;
else ans=2;
cout<<ans<<"\n";

6

https://codeforces.com/problemset/problem/1794/B

问:
给一个长为 \(n\) 的正整数 \(\{ a \}\) ,可以选择其中任意一个元素 \(+1\) ,不超过 \(2n\) 次。要求最后 \(a_i \nmid a_{i+1}, i=1,2, \cdots, n-1\) 。构造这个操作后的 \(\{ a \}\)

思考:
感觉很神奇,每相邻两个数,前一个不整除后一个。

首先知道,\(a_i = 1\) 是无论何时都不能出现的。很神奇的构造。

尝试一下能不能一次从左往右构造,否则从右往左,再否则按值考虑。

如果 \(a_1=1\) ,让 \(a_1\) 加上 \(1\) 。然后从 \(i=2\) 往右考虑。

  • \(a_{i}=1\) ,让 \(a_{i}\) 加上 \(1\) 变成 \(2\) 。再看是不是 \(a_{i-1} \mid a_{i}\) 。如果是则 \(a_{i-1}=2\) ,让 \(a_{i}\) 再加上 \(1\) 变成 \(3\) 就很好。否则不用操作。
  • \(a_{i} \geq 2\) ,看是不是 \(a_{i-1} \mid a_{i}\) ,如果是则 \(a_{i}\) 加上 \(1\) 即让 \(a_{i-1} \nmid a_{i}\) (因为 \(a_{i-1} \neq 1\) )。

那么最多不会超过 \(2n\) 次操作。

7

https://codeforces.com/problemset/problem/1779/B

问:
需要构造一个数组 \(s_1, s_2, \cdots, s_n\) ,满足:

  • 每个数都不是 \(0\)
  • 每个相邻数的和是整个数组的和。

给出 \(n \geq 2\) ,构造数组或回答不能。

思考:
如果 \(2 \mid n\) ,那么 \(1,-1\) 交替就符合题意。
样例说 \(n=3\) 不能构造,其实我们也能证明 \(a_1 + a_2 = a_2 + a_3 = a_1 + a_2 + a_3\) 当且仅当 \(a_1,a_2,a_3=0\)
\(n=5\) 能不能搞?\(a_1+a_2=a_2+a_3=a_3+a_4=a_4+a_5\) ,换句话说 \(a_{i}=a_{i+2}\) ,所以只能是奇数位置上的数一样,偶数位置上的数一样。
\(a_1+a_2=\sum_{i=1}^{n}a_i\) 可以表明 \(\sum_{i=3}^{n}a_i=0\)

好麻烦,先看 \(n=5\) 能不能搞。\(1,-2,1,-2,1\) ,满足整个数组的和是 \(-1\) ,然后相邻两个数的和是 \(-1\) 。那么 \(n \geq 5\) 的奇数都能这样搞了。

hack:
\(n=7\)\(1,-2,1,-2,1,-2,1\) 他们的和是 \(-2\) 。感觉有点逆天,我应该 \(n \geq 5\) 的奇数,比如 \(7\) ,验算一下。

我突然意识到我们可以让和为 \(0,1,-1\) 之间的一个并且容易证明。至于怎么来的,我也不知道,反正就是这个题的切入点很浅所以不太难想,靠着题感灵机一动就会想到(下面会说这种题到底应该怎么做)。

证明是,当 \(n\) 是奇数,我们考虑奇数的 \(\lfloor \frac{n}{2} \rfloor\) 个偶数位置,和 \(\lfloor \frac{n}{2} \rfloor + 1\) 个奇数位置。
让相邻两个数的和为 \(1\) ,不妨让多一位的奇数位置是 \(x\) ,让少一位的偶数位置是 \(-(x+1)\) ,这样相邻两位就是 \(-1\)
然后整体列一个方程,问题容易想到可以被解。

\[\begin{aligned} (\lfloor \frac{n}{2} \rfloor + 1) \times x + \lfloor \frac{n}{2} \rfloor \times (-(x+1)) = -1 \\ \lfloor \frac{n}{2} \rfloor \times x + x + \lfloor \frac{n}{2} \rfloor \times (-x) + \lfloor \frac{n}{2} \rfloor \times (-1) = -1 \\ x = \lfloor \frac{n}{2} \rfloor - 1 \\ \end{aligned} \]

这时显然也能发现,相邻两个数的和其实可以钦定,然后无脑解方程。

其实这个题也可以直接列方程,是直接解出来的。直接钦定奇数位置是 \(x\) ,偶数位置是 \(-(x+d)\) 就好。这是最好的方法,而不是归纳。

为什么要这么想呢,因为 \(a_i + a_{i+1}\) 都是一样的,那么如果是一样的,我们就可以钦定它,然后一整套方程下来。一个方便的设元就是 \(a_{i}=x,a_{i+1}=x+d\)

比如我们可以列出这样的式子

\[\begin{aligned} (\lfloor \frac{n}{2} \rfloor + 1) \times x + \lfloor \frac{n}{2} \rfloor \times (-(x+d)) = -d \\ \lfloor \frac{n}{2} \rfloor \times x + x + \lfloor \frac{n}{2} \rfloor \times (-x) + \lfloor \frac{n}{2} \rfloor \times (-d) = -d \\ x = \lfloor \frac{n}{2} \rfloor \times d - d \\ \end{aligned} \]

这里 \(d\) 随便取一个数其实也不影响答案正确。


这种 \(900\) 的题带一点讨论和枚举的我就要搞比较久,直接按某种自然顺序归纳的我就能比较快的归出来。

感觉是不是要总结一下讨论方法呢。

想了比较久甚至基本不知道怎么搞而是靠硬归纳+讨论的是 \(4,7\)

\(4\) 感觉是比较荒唐没啥切入点的构造,但是知道分奇偶讨论前缀和的 trick 其实会好很多。但是这个切入点还不够。基于自然并分奇偶考虑,这样就比较好切入。容易在自然顺序上把奇偶交换然后容易证明。

\(7\) 应该学好从哪个切入点列方程,就很容易做。

比较唐的是 \(5\) ,思维细节缺了,然后改过来其实还好。

其他归纳得都比较快而且都能对,大概五分钟。


8

https://codeforces.com/problemset/problem/1775/A2

问:
给一个长度 \(\geq 3\) 的字符串 \(s\) ,且成三个串 \(s_1,s_2,s_3\) ,换句话说 \(s=s_1+s_2+s_3\)\(s\) 只由 \(a,b\) 组成。要求满足条件:

  • 或者 \(s_2\) 的字典序满足 \(s_2 \geq s_1\)\(s_2 \geq s_3\)
  • 或者 \(s_2\) 的字典序满足 \(s_2 \leq s_1\)\(s_2 \leq s_3\)

思考:
其实这里的 \(a,b\) 可以看成 \(0,1\) 。然后是字典序的比较,这里的两个串比较要从前往后贪心的看。

  • 考虑让 \(s_2\) 的字典序最大,在字典序比较里不是很可控。我们考虑字典序最小会更可控,先考虑这个。
  • 考虑让 \(s_2\) 的字典序最小,我只需要让 \(s_2\)\(s\) 的非第一后最后一个,且 \(s_2=a\) ,然后 \(s_1,s_3\) 取满前后缀,总会有 \(s_2 \leq s_1,s_3\) 。证明是显然 \(s_2=a\) 是极小且最小。
  • 否则,就是说第 \(2 \sim n-1\) 个字符都没有 \(a\) ,那么全是 \(b\) ,那么我们取满这些位置,让 \(s_1,s_3\) 分别是第一个和最后一个字符,总会有 \(s_2 \geq s_1,s_3\) 。证明是显然 \(s_2\) 是极大。

代码不难写。主要是考虑问题的顺序,先从处理掉更可控的情况,那么剩下的情况总是当前状态下最可控的。

但是我好像不是很会下标怎么算,放一下代码。

如果在字符串 \(s\) 中找到一个索引 \(p\) ,那么它前面其实有 \(0,1,\cdots,p-1\)\(p\) 个数,后面有 \(p+1,p+2,\cdots,n-1\)\(n-1-p\) 个数。
再换个角度说,前缀是从 \(0\) 索引开始的 \(p\) 个字符,后缀是从 \(p+1\) 索引开始的 \(n-1-p\) 个字符。

view
cin>>s;
int n=SZ(s);
int p=-1;
L(i,1,n-2)if(s[i]=='a'){
	p=i; break;
}
if(p!=-1){
	cout<<s.substr(0,p)<<" "<<s[p]<<" "<<s.substr(p+1,n-p-1)<<"\n";
}else{
	cout<<s[0]<<" "<<s.substr(1,n-2)<<" "<<s.back()<<"\n";
}

9

https://codeforces.com/problemset/problem/1758/B

问:
构造一个长度为 \(n\) 的序列 \(\{ a \}\) 满足 \(1 \leq a_i \leq 10^{9}\) 且有

\[n \bigoplus_{i=1}^{n} a_i = \sum_{i=1}^{n} a_i \]

思考:
看到又有异或又有加法,第一反应是异或是不进位的加法,加法对异或有分配律。但是这对做这题好像没有什么帮助。
这还带一个乘法就很难搞。但是是乘以 \(n\) 而不是随便乘一个什么数。正常来说我们应该打个表硬做的,打表找规律磨时间总能磨出来。

我不明白我为啥看不出啥性质,涉及二进制的运算带个乘法就是很难搞。而且这题在二进制意义下我也不知道怎么归纳,难道这题真的要靠看题解?

稍微努努力,奇数肯定好做啊,全部构造成 \(x\) ,总有 \(n \bigoplus_{i=1}^{n} x = nx = \sum_{i=1}^{n} x\) ,那不妨是 \(1\) 好了。关键是偶数没有任何好做的地方啊。

等一下,我突然发现有个性质我没掌握,就是我们可以让异或空贡献。这个东西也很美妙啊我居然没直觉。

比方说 \(n=2k\) ,我让前 \(2k-2\) 个数空贡献,最后两个数是 \(x,y\) ,那么 \(n \bigoplus_{i=1}^{n} a_i = n(x+y)\)

比方说尝试让前面两两抵消,即都是 \(z\) 。会有 \(n(x \oplus y) = x+y+(n-2)z\) 。这里不能乱拆,分配律不总是能传递的。我们只需要保证 \(x \neq y\) ,不关心 \(z\) ,因为前面的 \(z\) 会全部被抵掉。

那我就随便构造嘛,让 \(x=1,y=2\) ,那么左边是 \(3n\) ,右边是 \(3+(n-2)z\) ,然后就有了 \(3(n-1)=(n-2)z\) 。看起来是没保证整数解的。

为了让式子更可控,肯定希望 \(n(x \oplus y)-(x+y)\)\((n-2)\) 的倍数,这样就只需要构造 \(x,y\) 。那就按自然顺序枚举 \(n-2\) 的倍数嘛,\(n-2\) 搞不出来,\(2n-4\) 发现诶可以搞了,因为可以让 \(x=1,y=3\) 。这样的话就有 \(2n-4=(n-2)z\) ,于是 \(x=1,y=3,z=2\) 。那么偶数的时候也解决了。

另外注意一下,我们只考虑了 \(n \geq 4\) 的偶数。那么 \(n=2\) 呢,直接按最后两个取 \(1,3\) 也是对的。

总结一下。奇数情况是显然的。偶数的时候考虑让异或空贡献,只让最后两个数不空,那么列出方程只有三个未知数。考虑一下可控性,就只需要考虑两个未知数,按自然顺序枚举一下就能找到答案。

然后二进制带乘法确实不好做,但是异或可以空贡献把问题规模搞得很小,然后就小规模比如两三个未知数的时候带乘法就也不难做了。

10

https://codeforces.com/problemset/problem/1747/B

问题:
\(s(n)\)\(n\)\(BAN\) 拼接在一起形成的字符串,显然 \(|s(n)|=3n\) 。可以进行以下操作任意次(可以是 \(0\) 次):

  • 选择 \(1 \leq i < j \leq 3n\)\(swap(s(n)_{i},s(n)_{j})\)

使得最终的 \(s(n)\) 不存在 \(BAN\) 的子序列。

要求构造一个操作序列,满足操作次数最少。

思考:

无非就是两种情况,考虑枚举每个 \(B\) ,要么是后面没有 \(A\) ,要么是后面有 \(A\) 但没有 \(N\)

很容易发现一个上界即 \(n\) 次,我们总可以把 \(B\) 丢到尾巴上。

实际上我们从前往后去看。比方说第一个 \(B\) ,后面有 \(n\)\(A\) 使得它可以形成 \(BAN\) 的子序列,那么把第一个 \(A\) 放到最后一个 \(N\) 的位置,那么还剩下 \(n-2\)\(A\) 。接着考虑下一个 \(B\) ,继续这个操作。于是我们又能发现总是可以一次操作让两个 \(A\) 失效,如果余下一个 \(BAN\) ,那么让当前这个 \(A,N\) 换位置。我们得到一个新的上界 \(\lceil \frac{n}{2} \rceil\) 。它不一定是最优,但是它足够优秀。感觉没有更优秀的了,然后对了。证明也不会证。

看看题解的证明。它的证明是说一次操作最多可以摧毁两个 \(BAN\) 的子串,让他们的 \(A,N\) 不再有任何贡献。我不知道他在说什么,但确实很有道理。

补充证明:
我们最开始只需要证明,一次操作最多可以让两个 \(A\) 失效。这个单次上界是显然的。
那么最少就需要操作 \(\lceil \frac{n}{2} \rceil\) 次,得到一个下界。
如果我们能构造出这个下界,那么我们就能声称它就是答案。实际上我们可以构造。
所以这题就被证明了。

实现:
那么考虑从前往后,比如说我当前这个指针是 \(i\) ,那么 \(n+1-i\) 和它是对称关系。那么 \(i \times 3 - 1\)\((n+1 - i) \times 3\) 交换就行。当 \(i\)\(n+1 - i\) 重合时这个交换依旧正确。所以我们枚举 \(i\)\(1\)\(\lceil \frac{n}{2} \rceil\)

总结:
但说白了,这题的切入点感觉就是有点玄而又玄,对我来说是这样的。只找性质,不背板,感觉这都不用打竞赛了。竞赛就需要背板。这个板就是很经典的板。

但它还是那个自然顺序的切入点,我们总可以发现一次操作可以让首尾两个东西一起贡献。只要往这个方向想了,那么问题就比较容易解决。

好像大概也能不太严谨地证明,反正非常对。


其实就这样了,高分的 trick 严格覆盖低分。刷 900 只是看看题型都是些啥。

你要说那些分奇偶前缀和、异或空贡献、钦定变量列式子、让构造更可控啥的,高分的其实都有,但是直接刷高分的,刷到了可能也不一定明白这是个 trick ,因为不清楚题型。

还有一些题,从性质角度出发,要找到题目的切入点就非常唐非常神奇。但是从固定切入点出发,背了板,一切又对得是那么合理。


1000 构造

1

https://codeforces.com/problemset/problem/1861/B

问:
给长度相同的字符串 \(a,b\) ,只包含 \(0/1\) 。每个字符串都是 \(0\) 开始 \(1\) 结束。你可以做以下操作任意次(可能是 \(0\) 次):

  • 选择某个字符串中的两个相同字符,然后让他们中间的字符变成和它们一样的字符

你要确定是否能够通过有限次操作使得 \(a,b\) 相同。

思考:
很重要的是每个字符串 \(0\) 开始 \(1\) 结束。这意味着假设 \(a,b\) 已经相同,我们依然可以选择把 \(a,b\) 变成前缀 \(0\) 和后缀 \(1\) 。那么一个更深刻的观察是,如果 \(a,b\) 可以通过操作变得相同,那么 \(a,b\) 也是可以变成相同的前缀 \(0\) 和后缀 \(1\) 。必要性也显然。

所以我们只需要找一个 \(i \in [1,n-1]\) 使得 \(a_{i}=0,a_{i+1}=1\)\(b_{i}=0,b_{i+1}=1\) 。那么两个字符串就可以相同。

view
cin>>str1>>str2;
auto work=[&](string str1,string str2)->string{
	int n=SZ(str1);
	L(i,0,n-2)if(str1[i]=='0'&&str1[i+1]=='1'&&str2[i]=='0'&&str2[i+1]=='1')return "YES\n";
	return "NO\n";
};
cout<<work(str1,str2)<<"\n";

对我来说是很轻松的观察,但是为什么 \(900\) 的几道难做很多,是因为 \(900\) 的题目没有背板吗。

2

https://codeforces.com/problemset/problem/1859/B

问:
\(n\) 个数组,第 \(i\) 个数组是 \(a_{i_{1}},a_{i_{2}},\cdots,a_{i_{m_i}}\) 。你可以操作以下操作任意次(可能是 \(0\) 次):

  • 选择某个数组,讲它的一个数移到另一个数组。要求是被增加元素的数组最多有一个,而每个数组最多只能移除一个元素。

我们认为这 \(n\) 个数组的价值是 \(\sum_{i=1}^{n} min_{j=1}^{m_i} a_{i_{j}}\) 。询问经过有限次操作后 \(n\) 个数组的最大价值。

思考:
那么其实就是说,最终代价是每个数组的最小值的和。要让他最大呢,每个数组的最小值先移除出去肯定是更好的,然后枚举一个数组把移除掉的元素都加进去。
假设第 \(i\) 个数组的最小值是 \(v_i\) ,次小值是 \(u_i\) ,那么代价就是 \(\sum_{i=1}{n} u_i - u_{j} + min_{i=1}^{n} v_i\) 。要让这个东西最大,那么枚举 \(j\)\(u_{j}\) 最小就好。

\(m_i \geq 2\) 就很好,不要再特判。

然后 hack 了,看了一下 \(n=1\) 居然没看到没考虑,但是考虑一下发现不影响。

然后看了一眼错误点,发现 \(n=1\) 确实不对,怎么会是啊。然后发现太唐乐,\(2^{29}<10^{9}\) 。我不如写成 \(MAX\_INT / 2\)

然后发现处理最小值和次小值没有背板,调了一段时间,这个核心代码得放。

这里我们要注意能更新最小值的时候要让次小值拷贝最小值的副本,再否则能更新次小值的之后只要更新次小值。

处理最小值和次小值
int m;cin>>m;
L(j,1,m){
	int x;cin>>x;
	if(mi[0]>x)mi[1]=mi[0],mi[0]=x;
	else if(mi[1]>x)mi[1]=x;
	chkmin(miv,mi[1]);
}
chkmin(res1,1LL*mi[0]);
res2+=mi[1];

为什么这两题的观察对我来说都很容易啊,只需要一些观察和分析。反而 \(900\) 的题要很多的归纳。

但是 \(900\) 的题要么归纳就总能归纳出,要么虽然看起来切入点很神秘但倾向于可背板。

3

https://codeforces.com/problemset/problem/1858/C

问:

  • 构造一个长度为 \(n\) 的排列 \(a_1, a_2, \cdots, a_n\)
  • \(d_i=gcd(a_i,a_{i \bmod n}+1)\)\(i=1,2, \cdots, n\)
  • \(\{d\}\) 的代价是 \(d_1, d_2, \cdots, d_n\) 中不同的数的个数。

你需要构造这么一个排列使得 \(\{ d \}\) 的代价最小。

思考:
我觉得这题很困难啊,你带个神秘的排列构造,还带个神秘 \(gcd\) ,然后又神秘地让 \(\{ d \}\) 中的数个数最小。

为什么只是 \(1000\) 的构造啊。

然后这里需要一些类似递降法的操作,具体展现在边界约束操作。首先我们可以注意到答案至少是能保证 \(\geq n\) 的,因为不可能 \(n\) 个数能出现多于 \(n\) 个不同的数。

然后我们发现相对大的值不能被表示,比如 \(n,n-1\) 显然不能作为两个数的 \(gcd\) 存在。更进一步的 \(\lceil \frac{n}{2} \rceil,\lceil \frac{n}{2} \rceil+1,n\) 都不能作为两个数的 \(gcd\) 存在,因为不难有重复的数,所以至少要看他们的 \(2\) 倍数,这已经 \(>n\) 了。

那么对于 \(\lfloor \frac{n}{2} \rfloor\) 是有办法作为两个 \(gcd\) 存在的,但我们不一定会去构造出这个数。

一步递降法之后,我们再尝试进行构造。注意到似乎可以两两配对。\(\lfloor \frac{n}{2} \rfloor,\lfloor \frac{n}{2} \rfloor \times 2\)\(\lfloor \frac{n}{2} \rfloor + 1,(\lfloor \frac{n}{2} \rfloor + 1) \times 2\) ,我们这样去顺序的放,似乎能刚好不重不漏地取满 \(n\) 个数(其实不能),使得 \(1,2,\cdots,\lfloor \frac{n}{2} \rfloor\) 都作为 \(gcd\) 出现。

那么我们用递降法将边界约束到 \(\leq \lfloor \frac{n}{2} \rfloor\) ,又构造了 \(\lfloor \frac{n}{2} \rfloor\) 的存在性。那么这就是答案。

然后写了一下代码发现不对劲。我们其实没办法不重不漏地取满 \(n\) 个数。比如 \(2,4,1,2\) 这种就很唐。

为什么唐呢,因为 \(x,2x,(x+1),2(x+1),(x+2),2(x+2),\cdots\) 就是很唐啊,很容易就重复了。

我直觉上感觉 \(\lfloor \frac{n}{2} \rfloor\) 就是答案,虽然没办法证明(如果能构造就等于证明了),但这种问题并不会有很高深的 trick ,能多次递降法约束边界。

经典的看看背板用没有用,能不能让小的和大的两两配对?不行。然后不会构造了……

然后突然有点感觉,灵光一闪,发现我漏了一个信息。一个数 \(x\) 最多只会被贡献两次,要么是一个相邻数的二分之一,要么是它的二倍。

那么我们可以这样构造,第一个数不妨就是 \(1\) ,总能让 \(gcd=1\) 出现。

然后 \(2 \times 2^{0},2 \times 2^{1},2 \times 2^{2},\cdots,2 \times 2^{p}\) ,使得 \(2 \times 2^{p-1} \leq \lfloor \frac{n}{2} \rfloor \leq 2 \times 2^{p}\) 。从 \(2\) 往后的数一直这样做,用类似埃氏筛法的方法边打标记边处理筛子。

这样的话 \(\leq \lfloor \frac{n}{2} \rfloor\) 的数就能作为 \(gcd\) 出现过。

那么这题就做完了。至于可以循环地构造就纯唐,这题根本用不到,出题人在搞神秘的乱搞。换句话说这不是题。

代码其实也能写一下的。

view
cin>>n;
tot=0;
L(i,1,n)if(!vis[i]){
	int x=i;
	while(true){
		a[++tot]=x;
		vis[x]=true;
		if(x*2>n)break;
		x*=2;
	}
}
assert(n==tot);
//		cerr<<"tot "<<tot<<" "<<n<<"\n";
L(i,1,n)cout<<a[i]<<" \n"[i==n];
L(i,1,n)vis[i]=false;

总结:
感觉好像,也就是稍微比其他题难观察了一点,对我来说……但本质上也是很简单的观察。
首先递降法的操作还是很直观。那么先用递降法约束到一个边界,再考虑构造,也是很经典。
再后面一个点,为什么我很难观察到呢……那么这好像是一个板,我记得应该是的。就是一个数可以按两种方式对左相邻和右相邻的数贡献

4

https://codeforces.com/problemset/problem/1844/B

问:
一个排列的代价是,他的子区间的 \(mex\) 是质数的个数。你需要构造一个长为 \(n\) 的排列,使得这个排列的代价最大,或者说这个排列的子区间的 \(mex\) 是质数的情况最多。

强调这里的 \(mex\)\(1\) 开始而不是 \(0\)

思考:
很清新很简洁的排列构造题,又带 \(mex\) 又带质数的。我们肯定是从小到大考虑正整数去构造 \(mex\)

\(mex=1\) 肯定和这个题目没有任何关系。然后考虑 \(mex=2\) ,那么实际上我们需要让 \(1\) 在一个区间内而 \(2\) 不在这个区间内。

经典地,位置 \(i\) ,能对哪些区间贡献呢?能对从 \(l=1,2,\cdots,i\)\(r=i,i+1,\cdots,n\)\(i \times (n-i+1)\) 个区间贡献。当 \(i = \lfloor \frac{n}{2} \rfloor = \lceil \frac{n}{2} \rceil\) 的时候(这一步错了),位置 \(i\) 能对最多的区间贡献。我们于是让 \(1\) 随意放在这两个位置里的一个,不妨是 \(\lfloor \frac{n}{2} \rfloor\) 。那么有意义的区间最多将只有 \(\lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor)\) 个。让可能有意义的区间最多,这步贪心大概率是对的。如果我们最终能证明我们真的可以让这些区间都有意义,那么这一步就是显然对的。

那我们再填入 \(2\) ,就会 \(ban\) 掉一些区间使得 \(mex \geq 3\) ,看起来就算填入了,如果能使得 \(mex=3\) 那也没啥影响。那怎么搞才能让问题更可控呢,我们把 \(2\) 填入到离 \(1\) 最远的那个位置,因为前面假定过 \(1\) 的位置,所以 \(2\) 要在位置 \(n\)

这使得我们可以让 \(l=1,2,\cdots,\lfloor \frac{n}{2} \rfloor\)\(r=\lfloor \frac{n}{2} \rfloor+1,\lfloor \frac{n}{2} \rfloor+2,\cdots,n-1\)\(\lfloor \frac{n}{2} \rfloor \times (n-1-\lfloor \frac{n}{2} \rfloor+1) = \lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor)\) 个区间给固定下来,这些区间一定有意义,他们的 \(mex=2\) ,后面不需要再考虑。

接下来,剩下的有意义的区间一定要覆盖两个点,即 \(1,2\) 的位置。这时候只有 \(r=n\) ,而 \(l=1,2,\cdots,\lfloor \frac{n}{2} \rfloor\) ,还剩共 \(\lfloor \frac{n}{2} \rfloor\) 个区间。

那么很好,让 \(3\) 放在位置 \(1\) 就好了,于是剩下的这些区间里,只有 \(l=1,r=n\) 这个区间没意义,这个区间的 \(mex=n+1\) (这个题目背景下)。其他 \(\lfloor \frac{n}{2} \rfloor\) 个区间都有意义。

那么其实我们已经证明了这样是对的。因为我们让 \(\lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor)-1\) 个区间都有意义了。如果有更优的解法,那么有意义的区间需要达到 \(\lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor)\) 这个上界。

如果改变最开始 \(1\) 的位置,上界达不到。所以 \(1\) 的位置是对的。因为我们 \(i \times (n-i+1)\) 如果稍微取小一点,上界就会小很多,这里就不证明了。
一个想法是,如果改变 \(2\) 的位置,这个上界还是达不到。但是比较难证明。
另一个想法是,因为 \(1\) 的位置能证明是对的,那么它一定对区间 \(l=1,r=n\) 有贡献,而这个区间肯定没意义。所以这样就能证明我们的答案是对的。

总结来说,就是一些观察、归纳、公式比较。

实现上 \(n \geq 3\) 时让 \(3\) 放在位置 \(1\)\(2\) 放在位置 \(n\)\(1\) 放在位置 \(\lfloor \frac{n}{2} \rfloor\)这里有问题,后面改了),其他地方随便放。\(n=1,2\) 时随便放最终导致的结果一样。这里最好写老实一些,直接按标记填数,像下面那样。

hack
\(i=\lceil \frac{n}{2} \rceil\) 时,能对 \(\lceil \frac{n}{2} \rceil \times (n - \lceil \frac{n}{2} \rceil + 1)\) 个区间贡献,取到最大值。
这个值要比 \(\lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor + 1)\) 大,因为

\[\begin{aligned} &\lceil \frac{n}{2} \rceil \times (n - \lceil \frac{n}{2} \rceil + 1) - \lfloor \frac{n}{2} \rfloor \times (n - \lfloor \frac{n}{2} \rfloor + 1) \\ = &(n+1) \times(\lceil \frac{n}{2} \rceil - \lfloor \frac{n}{2} \rfloor) + \lceil \frac{n}{2} \rceil \times (-\lceil \frac{n}{2} \rceil) - \lfloor \frac{n}{2} \rfloor \times (-\lfloor \frac{n}{2} \rfloor) \\ = &(n+1) \times(\lceil \frac{n}{2} \rceil - \lfloor \frac{n}{2} \rfloor) + (\lfloor \frac{n}{2} \rfloor)^{2} - (\lceil \frac{n}{2} \rceil)^{2} \\ \end{aligned} \]

\(n=2k\) ,这个式子是 \(0\) 。否则当 \(n=2k+1\) ,这个式子是

\[n+1 + (\frac{n-1}{2})^{2} - (\frac{n+1}{2})^{2} = 2k+2+k^{2}-(k+1)^{2} = 2k+2+k^{2}-(k^{2}+2k+1)=1 \]

所以 \(1\) 的位置一定是在 \(\lceil \frac{n}{2} \rceil\)\(2\) 的位置一定是在 \(n\)\(1\) 的位置一定是在 \(1\)

view
cin>>n;
if(n<=2){
	L(i,1,n)a[i]=i;
}else{
	a[(n+1)/2]=1,a[1]=3,a[n]=2;
	tot=3;
	L(i,1,n)if(!a[i])a[i]=++tot;
}
L(i,1,n)cout<<a[i]<<" \n"[i==n];
L(i,1,n)a[i]=0;

一点小细节是这里的 \(tot\) 一开始写成了 \(4\) ,因为我是想从 \(4\) 开始连续填入。那么当我打算

  • \(++tot\) 的时候,已经说明了在执行 \(++tot\) 前,现在的 \(tot\)上一个被填入的值。换句话说最终 \(tot\)闭的区间
  • 否则我们认为 \(tot\)现在的值\(tot++\) 后则是下一个值。换句话说最终 \(tot\)开的区间

但是我感觉这题让我没啥实力提升啊???就是很经典的按 mex 从小到大考虑,约束一个可能的上界,然后构造证明这个上界是对的。
反而是怎么考虑一些以前的错误、怎么写代码,让我提升了。比如 tot 怎么用。比如上取整的位置能贡献给最多区间。

5

https://codeforces.com/problemset/problem/1837/C

问:
给予一个字符串 \(s\) ,只包含 \(0,1,?\) 三种字符,称呼它是模式串。如果一个字符串可以匹配这个文本串,当且仅当 \(s\) 中的 \(?\) 替换成 \(0\)\(1\) 后,\(s\) 和这个串一样。

定义一个字符串的代价是最小操作数是反转连续一段子串的次数,使得最终它按照升序排序。

你需要将 \(s\) 变为一个和他匹配的字符串,并且代价最小。

思考:
假设我们有一个 \(01\) 字符串 \(t\) ,那么最终通过若干次反转连续子串后,能变成 \(000\cdots111\) 的形式。

怎么让 \(t\) 通过最小次数的操作变得有序呢?比如首先我们需要把前缀 \(0\) 和后缀 \(1\) 忽略掉,这些位置是没必要动的。重新定义 \(l,r\) ,使得 \(t_{l}=1,t_{r}=0\)

接下来 \(t\) 的状态是这样:连续的 \(1\) ,连续的 \(0\) ,连续的 \(1\) ,连续的 \(0\) ,……,连续的 \(1\) ,连续的 \(0\) 。不难证明对于每段“连续的 \(1\) ,连续的 \(0\)” ,至少要经过一次操作,并且我们可以只进行一次操作。于是操作总是就是大小为二的子串 \(10\) 的数量。

好现在我们有一个模式串 \(s\) ,里面有 \(0,1,?\) 。我们需要把 \(?\) 改成 \(0\)\(1\) 。实际上根据上面的讨论,我们要把 \(s\) 变成文本串使得其中的二子串 \(10\) 最少。

对于 \(s\) 中的一个 \(?\) 字符。我们可以从左到右去看。如果刚好是 \(s_1\) ,那么 \(0\) 会比 \(1\) 优秀,因为会让后序不出现潜在的二子串 \(10\) 。否则,我们只看它左边一个字符 \(x\) ,若 \(x=0\) ,则当前的 \(?\) 字符变成 \(0\) 会比变成 \(1\) 更优秀,若 \(x=1\) ,则当前的 \(?\) 字符显然只能变成 \(1\)

实现的时候一个比较唐的地方是,我应该写 s[i]='0',s[j]='1' 而不是 s[i]=0,s[j]=1 。

总结:好像就是一些简单的观察,自然而然就能作对了?为啥呢。就是一些简单的观察啊。感觉背板的地方可以背重新定义 \(l,r\) ,让字符串变成全是连续的 \(1\) 接上连续的 \(0\) 。实现上要背板的地方是,注意赋值字符要赋值成字符而不是数字。

view
cin>>s;
int n=SZ(s);
L(i,0,n-1)if(s[i]=='?'){
	if(i==0||s[i-1]=='0'){
		s[i]='0';
	}else{
		s[i]='1';
	}
}
cout<<s<<"\n";

6

https://codeforces.com/problemset/problem/1831/B

问:
给两个长度为 \(n\) 的数组 \(\{a\},\{b\}\) 。你需要归并这两个数组使得他们变为一个长度为 \(2n\) 的数组 \(\{c\}\)
你需要找到 \(\{c\}\) 中最大连续相同段的长度。不需要构造出 \(\{c\}\)

思考:
归并就是……比方说我先把 \(\{a\}\) 的一段非空前缀移到 \(\{c\}\) 前面。那么操作就是一段 \(\{a\}\) 的非空前缀,一段 {b} 的非空前缀,一段 \(\{a\}\) 的非空前缀,一段 {b} 的非空前缀…… 直到 \(\{a\},\{b\}\) 为空。或者我们可以先把 \(\{b\}\) 的一段非块前缀移到 \(\{c\}\) 前面。

有了这件事情,那么本质上我们可以用一个 \(v_{1}\) 数组计数,\(v_{1}(x)\) 表示 \(x\)\(\{a\}\) 数组中连续出现的最多次数。这可以用双指针做到。同样的 \(v_{2}\) 数组对 \(\{b\}\) 奇数。

这样我们便利值域,\(v_{1}(x)+v_{2}(x)\) 的最大值就是答案。然后 \(a_i,b_i \leq 2 n\) 所以直接遍历整个值域就行,否则需要把要查的数离散化出来。

因为双指针写起来还是比较有意思的,这里放一下代码,并且说一下这种情况下双指针应该怎么写。

这里的双指针不是快慢指针,而是分段开区间。
……然后,很好,果然双指针又写错了。为什么呢?因为啊。
\(j\) 找到的是闭区间,让 \(i=j\) 显然很合理,但是后面 for 循环了 \(i\) 又会自增 \(1\) ,那就很不合理。那么我们让 \(i\) 到右端点闭区间的位置就好了,即 \(i=j-1\)

view
L(i,1,n)cin>>a[i];
L(i,1,n)cin>>b[i];
L(i,1,n){
	int j=i;
	while(j<=n&&a[j]==a[i]){
		j++;
	}
	chkmax(v[0][a[i]],j-i);
	i=j-1;
}
L(i,1,n){
	int j=i;
	while(j<=n&&b[j]==b[i]){
		j++;
	}
	chkmax(v[1][b[i]],j-i);
	i=j-1;
}
ans=0;
L(i,1,2*n){
	chkmax(ans,v[0][i]+v[1][i]);
}
//		cerr<<"ans "<<ans<<"\n";
cout<<ans<<"\n";
L(i,1,2*n)v[0][i]=v[1][i]=0;

7

https://codeforces.com/problemset/problem/1816/B

前话:我记得这是个比较神仙的构造题,对于我来说,很牛逼。

问:
给一个 \(2 \times n\) 的网格图,需要将 \(1,2,\cdots,2n\) 这个排列放入网格图中。
一个数组 \(a_1,a_2,\cdots,a_k\) 的价值是 \(a_1 - a_2 + a_3 - a_4 + \cdots = \sum_{i=1}^{k} a_i \times (-1)^{i+1}\)
从网格图的 \((1,1)\) 开始,到 \((2,n)\) 结束,只能向下或向右走,可以得到一条长度为 \(n+1\) 的路径,路径上顺序经过的数代表这个路径的数组。
构造一个网格图,使得这个网格的所有路径的价值满足最大的价值最小。

思考:
一般来说,最大的价值最小这个字眼很可能是明示二分,但这里显然不是啊。因为这是个构造题。
然后, 所有
可以证明所有路径尽量平均,可以实现最大的价值最小。这个证明一时半会不好证,但是可以证,我记得我也证过。

  • 一个更一般但也常见情况是,只有两种情况时,两种情况取 \(\lfloor \frac{n}{2} \rfloor,\lceil \frac{n}{2} \rceil\) ,这时候是最大的最小。
  • 另一个比较难证但是正确的结论是,邻项交换,也可以说是微扰法,构造偏序的时候通常有用。

好,那么还是看这题。我们应该反应出来,我们其实需要构造一个东西,满足两个条件:

  • 所有路径的价值尽量平均。
  • 平均值尽可能大。

这是第一个 \(trick\)

第二个 \(trick\) 是,如果网格图只能向下或者向右,那么实际上它的对偶图可以看成按斜线分层,并且每层上的横纵坐标之和是奇偶交替的。

发现只有 \(n\) 条路径,即 \([p_{1,1},p_{2,1},p_{2,2},p_{2,3},\cdots,p_{2,2n}]\)\([p_{1,1},p_{1,2},p_{2,2},p_{2,3},\cdots,p_{2,2n}]\)\([p_{1,1},p_{1,2},p_{1,3},p_{2,3},\cdots,p_{2,2n}]\) ,……,\([p_{1,1},p_{1,2},p_{1,3},\cdots,p_{1,2n},p_{2,2n}]\)

我们怎么让平均值尽可能大呢,注意到 \(1+1\)\(2+2*n\) 都是偶数,都是正的贡献。最好的构造只能是这两个位置填入最大的 \(2n,2n-1\)

那么看接下来的位置,定义 tot=2n-2 ,我们先 for 所有合法的偶数位置,比如说 \(2 \mid (2+i)\) ,那么让 a[2][i]=tot--, a[1][i+1]=tot-- 。

再 for 所有合法的奇数位置,a[2][i]=tot--,a[1][i+1]=tot-- 。

这么做的好处是:

  • 我们让更大的那些数放在正的贡献里,更小的数放在负的贡献里。依然保证了平均值会尽可能大。
  • 路径 \(1\) 到路径 \(2\) 的变化是少减去 \(1\) ,路径 \(2\) 到路径 \(3\) 的变化是少增加 \(1\) ,路径 \(3\) 到路径 \(4\) 的变化是少减去 \(1\) ,路径 \(4\) 到路径 \(5\) 的变化是少增加 \(1\) ,…… 。这使得每条路径的代价都尽量平均。

那么我们就完成了限制条件下的构造。

总结:
这题对我来说有点折磨,大概是因为我之前对这里的两个 trick 不太懂。

8

https://codeforces.com/problemset/problem/1772/C

问:
给正整数 \(k,n\) 。构造一个长度为 \(k\) 且值在 \([1,k]\) 范围内的严格递增数组 \(a_1, a_2, \cdots, a_k\) 。使得这个数组的差分数组 \(a_2-a_1, a_3-a_2, \cdots, a_k - a_{k-1}\) 中,不同的数最多。

思考:
那如果可以,就是无脑让差分数组是 \(1,2,3,\cdots\) 。这么做会使得上界越过 \(n\)
于是,我们可以让差分数组是 \(1,2,3,\cdots,x-1,x,1,1,\cdots,1\) 。这个 \(x\)\(1\) 枚举到 \(n\) 就行了,可以 \(O(1)\) 检查的话就很好。
再不济 \(x\) 肯定是单调的,如果只能 \(O(n)\) 检查,那也可以二分 \(x\)

假设有了一个 \(x\) 。首先根据叠加有 \(1 + 2 + \cdots + x = (a_2 - a_1) + (a_3 - a_2) + \cdots + (a_{x + 1} - a_{x})\) 。于是有 \(\frac{x(x+1)}{2} = a_{x+1}-1\) 。那么后序还有 \(a_{x + 2}, a_{x + 3}, a_{k}\)\(k - (x + 1) = k - x - 1\) 个数的空间,显然让 \(a_{x + 1 + y} = a_{x + 1} + y\) 可以尽可能不越过上界。

换句话说我们算得 \(a_{x + 1} = 1 + \frac{x(x+1)}{2}\) ,于是 \(a_{k} = a_{x + 1} + k - (x + 1) = 1 + \frac{x(x+1)}{2} + k - x - 1 = \frac{x(x+1)}{2} + k - x\) 。(算得 \(a_{x+1}\) 是没有任何必要的

那么我们就可以 \(O(1)\) 检查 \(x\) 了。

如果 \(x\) 是合法,那么我们这样构造。对于前 \(x+1\) 个数,枚举 \(i \in [1,x+1]\) ,第 \(i\) 个数,我们需要有 \(a_{i+1}-a_{i}=i\) ,且钦定 \(a_{1}=1\) 。注意下一边界即可。

后面的数都让 \(a_{i + 1} = a_{i} + 1\) 就能构造。

实际上可以 \(O(n)\) 从大到小枚举查询 \(x\) 就好。可以优化到 \(O(\log n)\) 二分查询 \(x\)

随便放个代码,需要注意一下边界。

view
cin>>k>>n;
R(x,n,1){
	if(x*(x+1)/2+k-x<=n){
		a[1]=1;
		L(i,1,k-1){
			a[i+1]=a[i]+(i<=x?i:1);
		}
		L(i,1,k)cout<<a[i]<<" \n"[i==k];
		break;
	}
}

总结:
感觉这题算得不是很流畅。实际上我们可以背一下板,如果得到了完整的差分数组,那么我们可以反解原数组任意一个位置。

比如这里 \(1,2,3,\cdots,x-1,x,1,1,\cdots,1\) 。首先要注意 \(1,2,\cdots,x\) 的部分是 \(i=a_{i+1}-a{i}\) ,也能得到后面有 \(k-x-1\)\(1\)

我们要反解 \(a_{n}\) ,就是由叠加有 \(\sum_{i=1}^{x} i + k-x-1 = a_{k} - 1\) ,然后反解到 \(a_{k}\)

所以我们上面先其实解出了 \(x\) 对应的 \(a_{x+1} - a_{x}\) ,这是没有任何必要的。因为可以直接反解 \(a_{k}\) (注意我们显然依然可以反解任何 \(a_i\) )。

但是这个思维方式还是比较奇怪。一个更好的思维方式是,\((a_2 - a_1),(a_3 - a_2),\cdots,(a_p - a_{p - 1})\) 是从 \(1\) 开始连续递增的。\(a_{p+1},\cdots,a_{k}\) 都是 \(1\) 。我们关注枚举这个下标,而不是这个值,那么思维会顺畅非常多。

总之还是很简单的题,关键是怎么写快写对。

再说一遍,这题关注下标会比关注值顺畅非常多。

9

https://codeforces.com/problemset/problem/1770/B

问:
给两个正整数 \(n,k\)\(p\) 是一个长度为 \(n\) 的排列,\(\{c\}\) 是一个疮毒为 \(n-k+1\) 的数组并满足:

\[c_{i}=max(p_i, p_{i+1}, \cdots, p_{i+k+1} + min(p_i, p_{i+1}, \cdots, p_{i+k+1}) \]

构造一个排列 \(p\) ,使得它确定的 \(\{c\}\) 的最大值最小。

思考:
比较神奇的构造题。如果能让 \(c_i\) 尽量平均,那么他就有最大值最小。也就是说越大的尽量和越小的配对。
\(k=1\) ,那没得玩,\(\{c\}\) 的最大值一定是 \(2n\)
\(k \geq 2\) ,可以考虑 \(n,1,n-1,2,n-2,3,\cdots\) 。就可以让 \(c_i\) 尽量平均。不清楚对不对,但感觉非常对,好像这题和 \(k\) 没有关系。

果然你妈对了。这是题吗?这是傻逼烟雾弹。单次就是构造尽可能平均的代价。我都不想证了,背板算了。这个板应该是很重要的。

10

https://codeforces.com/problemset/problem/1761/B

问:
给循环序列 \(s_1, s_2, \cdots, s_n\) ,这意味着 \(s_n\) 右边一位是 \(s_1\) 。如果这个序列相邻两个数相同,那么这两个立刻会被擦除一个。

一开始序列没有两个相邻的元素。要求执行以下操作最多次,知道序列变成空:

  • 选择序列中一个元素,手动擦除它。

你需要回答这个操作数,不需要构造操作方案。

思考:
就是稍微按着更可控的顺序,从简单往难归纳,考虑比较朴素的情况。
如果 \(\{s\}\) 中每个数都不相同,那么需要且只能操作 \(n\) 次。
如果 \(\{s\}\) 中存在一对一开始不相邻相同的数 \(x\) ,我们先擦除其中一个 \(x\) ,接下来 \(n-1\) 个数我们都可以随意删除,最多可以操作 \(n\) 次。
如果 \(\{s\}\) 中存在相当多对的一开始不相邻的相同数,但不至于某对数的其中一个被擦除,一定导致另一对相邻,那么我们也可以最多操作 \(n\) 次。
现在考虑到底能接受多少对相同的数。

  • 如果 \(2 \nmid n\) ,有 \(\lfloor \frac{n}{2} \rfloor\) 对是可接受的,多出来的那个数如果是 \(y\) 且和每对数都不同,我们总可以擦除 \(y\) 右边那个数,最后擦除 \(y\)

这个方向确实可以继续想下去,但我觉得可能想复杂了,因为我灵机一动,发现了一个性质。如果值 \(\geq 3\) ,那么我们总可以用最多的操作,删除到只剩下两种值,使得这两种值交替排布。

为什么会灵机一动呢,因为比较容易可以尽可能想到构造唯一的 \(y\) ,借助它去删掉其他数。这种情况在规模相对大的时候几乎总是可以构造。

这就是所谓迭代加深的思考方式吧。

实际上,如果值 \(\geq 3\) ,我们可以留下一个额外的值,加上最终剩下的另外两种值。我们可以借助这个唯一的额外值,完美消去另外两种值。于是这时候最大操作数是 \(n\)

那么我们只需要考虑只有 \(2\) 中值的情况,数据一定是交替排布的。规模相对大的时候,每擦除一个数,一定会有另一个数被带走。规模很小的时候,比如 \(xyxy\) ,我们擦除一个数,被带走一个数。剩下 \(xy\) ,接着需要两次擦除。也就是只剩下两个数的时候做擦,不会产生额外的带走,于是我们需要多一次操作。于是这是的最大操作数是 \(n/2+1\)

总结:
这种存在相邻相同数则会抵消一个的结构。如果可以擦除,我们肯定有一种方案是不产生任何抵消,使得最终只剩下两个数交替排布。显然我们也可以反悔最后一次擦除,使得存在第三个唯一的数。这种结构,对交替排布的结构进行观察很可能是关键的。

1100 构造

1

https://codeforces.com/problemset/problem/2044/D

这题其实还是比较唐,我和我两个退役朋友一起写这场 d4 ,结果他们基本上十几分钟内都 ac 了,但我当时瞪了十几分钟其实没有一点思路。

问:
给一个长度为 \(n\) 的序列 \(\{ a \}\) ,构造一个长度为 \(n\) 的序列 \(\{ b \}\) ,使得 \(a_i\)\(b_1, b_2, \dots, b_i\) 的众数。

\(1 \leq 10^{5} \leq n\)

思考:
一开始想起来,感觉相对来说还是有点匪夷所思。需要一些 trick ,众数不一定唯一。

更深刻地应用 trick 是,如果我们构造的序列 \(\{ b \}\) 满足任意两个数不同,那么它的众数可以是任意一个元素。序列 \(\{ b \}\) 的前缀也有这样的性质。

那么我们从前往后遍历序列 \(\{ a \}\) ,构造 \(\{ b \}\) 的前缀使得任意两个数不同,就是容易的。

如果对 \(\{b\}\) 的值域没有限制太多。具体方案是,若在 \(\{ a \}\) 遇到一个未出现的数 \(x\) ,则让当前 \(\{b\}\) 的元素等于 \(x\) ,否则填入一个之前和之后都不出现的数 \(y\) ,比方说从值域上界 \(+1\) 开始初始化 \(y\) 并自增。

如果限制了 \(\{b\}\) 的值域,那么让 \(y\) 初始化为 \(1\) ,填入它之前,让它循环自增,直到是一个 \(\{a\}\) 中没有出现过的数。填入后再自增一次。自增的均摊时间是 \(O(n)\) 的。

很轻松的实现:

view
int _;
for(cin>>_;_;_--){
	cin>>n;
	L(i,1,n)v[i]=0;
	S.clear();
	y=1;
	L(i,1,n){
		int x;cin>>x;
		v[a[i]=x]++;
	}
	L(i,1,n){
		int x=a[i];
		if(S.count(x)){
			while(true){
				if(!v[y])break;
				y++;
			}
			cout<<y++<<" \n"[i==n];
		}else{
			cout<<x<<" \n"[i==n];
		}
		S.ist(x);
	}
}

2

https://codeforces.com/problemset/problem/1682/B

问:
给一个 \(0 \sim n-1\) 的排列 \(p\) ,开始时这个排列是非有序的,即开始时存在一个位置 \(i\) 满足 \(p_i > p_{i+1}\)

定义一个排列 \(p\)\(X\) 排序的,当且仅当进行有限次以下操作可以使得 \(p\) 有序:

  • 选择 \(p_i \ AND\ p_j = X\)
  • \(swap(p_i,p_j)\)

找到最大的 \(X\) 使得排列 \(p\)\(X\) 排序的。

思考:
乍一看好像很困难,有点匪夷所思。
第一件事:

假设给一个 \(X\) ,如何判断一个排列是 \(X\) 排序的?

我们可以根据按位与的性质,任意两个元素如果可以交换,当且仅当: \(X\) 上某一位是 \(1\) 当且仅当这两个元素的这一位同时是 \(1\)

这些可交换的元素会构成一个集合,然后是可以任意两两交换的。不难推出这个集合的元素可以任意排序,但是占据的位置不能改变

所以我们需要判断,不可交换的元素的集合,每个元素都是当前的位置。比如元素 \(4\) 不能交换,则一定是在第 \(4+1\) 位。

第二件事:

假设已知一个排列,如何找到任意一个 \(X\) 使得这个排列是 \(X\) 排序的。

首先我们找到所有不在本身位置上的元素,即 \(i\) 不在第 \(i+1\) 位上,让他们组成集合 \(\mathbb{S}\) 。将他们异或起来得到 \(Y\) ,可以保证 \(\mathbb{S}\) 中的元素,在某一位上都是 \(1\) ,当且仅当 \(Y\) 在这一位上也是 \(1\)

不难证明这时候的 \(Y\) 显然是 \(X\) 的一个上界。因为 \(Y\) 某一位不是 \(1\) ,则存在 \(x \in \mathbb{S}\)\(x\) 这一位不是 \(1\) 。而这个 \(x\) 一定无法从 \(\mathbb{S}\) 中移除。

这是题吗?……

view
cin>>n;
And=numeric_limits<int>::max();
L(i,1,n){
	int x;cin>>x;
	if(x!=i-1)And&=x;
}
cout<<And<<"\n";

3

https://codeforces.com/problemset/problem/1867/B

问:
给你一个长度为 \(n\)\(01\) 序列 \(s\) 。如果一个 \(x\)\(good\) 的,当且仅当存在一个有 \(x\)\(1\)\(01\) 序列 \(l\)\(s_i \oplus l_i\) 得到的新序列是回文的。

你需要输出一个长度为 \(n+1\)\(01\) 序列,表示 \(x=0,1,2,\cdots,n\) 是否是 \(good\) 的。\(1\) 表示是,否则不是。

思考:
考虑异或是不进位的加法,当且仅当 \(0 \oplus 0\) 左值不会改变。
\(x=0\) ,则只需 \(s\) 是回文的。
\(x>0\) ,则只需翻转 \(s\)\(x\) 个数,\(s\) 是回文的。

那么我们可以事先求出,至少翻转 \(s\) 中几个数,可以使得 \(s\) 是回文的,不妨是 \(y\) 个。
那么当 \(|s|\) 是奇数,翻转 \(s\)\(\geq y\) 个,\(s\) 都可以是回文的。
\(|s|\) 是偶数,翻转 \(s\)\(y+2k\) 个,\(s\) 是回文的。而可以证明,翻转 \(y+2k+1\) 个,\(s\) 没办法回文。根据回文的对称性,我们没办法对于一个 \(|s|\) 为偶数的回文串上,改变奇数个字符,使得他是回文的。

实现:
暴力 \(O(n)\) 找到最小的 \(y\) 是翻转 \(s\) 中最少的数使其回文,即不对称的字符对数量。\(<y\) 的部分显然做不到,\(\geq y\) 的部分按 \(|s|\) 的奇偶性处理。

hack:
每个位置最多可以翻转一次,所以不仅有下界是 \(y\) ,还有上界。如果剩下最后 \(y\) 个位置,显然是最开始翻转的 \(y\) 个字符匹配的位置,那么这些位置一定不能改变,于是上界是 \(n-y\) 。可以证明找不到其他更紧的上界,因为我们可以证明 \(n-y-1\) 或者 \(n-y-2\) ,不是上界或者不可行。

代码

view
cin>>n;
cin>>s;
cnt=0;
L(i,0,(n-1)/2)cnt+=s[i]!=s[n-1-i];
L(i,0,n){
	if(i<cnt)cout<<0;
	else if(i>n-cnt)cout<<0;
	else cout<<(n%2==0?((i-cnt)%2)^1:1);
}
cout<<"\n";

4

https://codeforces.com/problemset/problem/1864/B

这题一看就很唐,很弱智。但是前段时间我居然忘了这种弱智题怎么做,估计是理解不够深刻。

问:
给一个长为 \(n\) 的小写字符串 \(s\) ,一个正整数 \(k\) 。每次操作可以选择如下任意一种操作执行:

  • 选择 \(i(1 \leq i \leq n-2)\)\(swap(s_{i},s_{i+2})\)
  • 选择 \(i(1 \leq i \leq n-k+1)\) ,反转区间 \([i,i+k-1]\)

你可以执行任意次操作(可能是 \(0\) 次),找到经过优先次操作后,能够让 \(s\) 变成的最小字典序的字符串。

思考:
首先第一种操作是让奇数位置的字符集合和偶数位置的字符集合,都是可以在不改变集合占据位置的情况下任意排列的。
一个好的理解模型是让相邻两个奇数位置和相邻两个位置连边,得到两个图。对于每个图,我们可以在不改变节点集位置的情况下,经过优先步任意重排节点。

那第二种操作是在干嘛?注意到反转操作后,我们可以加入新的边。
于是我只需要通过反转操作,使得存在一个元素,可以到和他原本不同奇偶性的位置上,但又不使得整个奇数集和偶数集位置互换,那么奇数集和偶数集就可以联通。

题目保证 \(k\)\(<n\) 的,我们于是甚至不需要考虑整个奇偶集互换的情况,因为当且仅当 \(k=n\) 存在这种情况。

于是,如果奇偶集可以联通,那么答案就是最小字典序排序。否则奇数位置集按最小字典序排序,偶数位置集按最小字典序排序。当 \(k \neq n\) ,当且仅当 \(k\) 是偶数,那么奇偶集可以联通。证明可以是一个偶数区间的首尾端点总是奇偶性不一样。

实现:
实现的时候又唐了。
首先 \(sort\) 的比较逻辑是元素而不是下标。

  1. 有些时候,对于下标数组 \(p\) ,我们用下标比较,是因为 \(p\) 本身的元素就是原数组的下标。更深刻的说,下标数组 \(p\) 是一个下标置换。
  2. 怎么比字符字典序呢?
    如果直接 sort 一个 string ,那么就是最直接的字典序最小。
    或者对于 char 数组,传入参数 char a,char b ,去比 a<b 。
  3. 怎么比字符串字典序呢?
    我们只能用 string 数组,传入 string a,string b ,去比 a+b<b+a 。

5

https://codeforces.com/problemset/problem/1838/B

问:
给一个大小为 \(n\) 的排列 \(p\) ,你需要使得它的子数组排列最少。并且你必须执行以下操作一次:

  • 选择 \(i,j(1 \leq i < j \leq n)\) ,然后 \(swap(p_i, p_j)\)

输出你选择的两个索引。

\(3 \leq n \leq 2 \cdot 10^{5}\)

思考:

  1. 那这样的话,看起来是排列问题,其实是 \(mex\) 问题。对于任意 \(m\) ,你需要让大小为 \(m\) 的区间,它的 \(mex=m+1\) 的情况最少。
  2. 接着考虑,我们从 \(mex\) 从小到大考虑,容易注意到,我们最多会有 \(n\) 个这样的区间,这是一个很好的上界观察。进一步地,我们可以将上下界约束到 \([2,n-1]\)

本质上,只需要让 \(1,2\) 尽可能远离。如果 \(1,2\) 的最大间距是 \(k\) ,那么会存在 \(n-k+1\) 个子排列。可以证明这是个下界,因为没办法构造更小的下界。
实际上只有 \(4\) 种可能的操作,\(swap(1,loc(1))\)\(swap(loc(1),n)\)\(swap(1,loc(2))\)\(swap(loc(2),n)\) 。执行这些操作后,一个数会钦定在 \(1\)\(n\) ,然后和另一个数的位置计算距离即可。这期间可以维护答案。

hack:
样例你都没手测过,你更我说你程序能对?
考虑样例的排列 \(8\ 7\ 6\ 3\ 2\ 1\ 4\ 5\ 9\) ,如果改成 \(2\ 7\ 6\ 3\ 8\ 1\ 4\ 5\ 9\) ,那么 \(9\) 排列和 \(8\) 排列都存在。
如果改成 \(8\ 7\ 6\ 3\ 9\ 1\ 4\ 5\ 2\) ,那么只存在 \(9\) 排列。

二次思考:
我们不从 \(mex\) 考虑,依然容易注意到,最多会有 \(n\) 个区间,满足这个区间是一个排列。那么我们去约束上界就行。
上界是 \(n\) ,显然这个 \(n\) 排列一定存在。
如果 \(n-1\) 排列存在,那么一定有一个大小为 \(n-1\) 的区间包含 \(1 \sim n-1\)
如果 \(n-2\) 排列存在,那么一定有一个大小为 \(n-2\) 的区间包含 \(1 \sim n-2\)
如果 \(k\) 排列存在,那么一定有一个大小为 \(k\) 的区间包含 \(1 \sim k\)
猜测,存在一个 \(x\) ,满足存在 \(y \leq x\) 的区间包含 \(1 \sim y\) ,而任意 \(z > x\) 的区间,无法包含 \(1 \sim z\) 。换句话说是猜测单调性。

但是实际上仔细分析,这个东西没办法导致单调性。比方说遍历 \(i=1 \sim n\) ,维护 \(l=min(pos(i))\)\(r=max(pos(i))\) ,首先显然 \(r-l+1>=i\) ,然后当且仅当 \(r-l+1=i\)\([l,r]\) 是一个排列。证明是 yes is yes,no is no。而这个东西显然可以构造出一些端点 \(i\) 使得函数在有解和无解的区间不停抖动,换句话说容易证明不具备单调性。

三次思考:
难道要止步于此了吗?这个题给我我确实一时半会不会做啊。我找不到他的 trick 。再稍微努力一下,能不能数据结构套板?比如说枚举左端点,快速查询右端点?稍微想一下就知道这种题显然不是数据结构啊,这个套版也做不了。三次思考——放弃思考。接下来是看题解。

看题解:
我们考虑这么一件事情,对于任意一个大小为 \(m\) 的子区间,如果是一个 \(m\) 排列,那么它一定包含 \(1,2, \cdots, m\),而不包含 \(m+1,m+2,\cdots,n\)
\(m \geq 2\) ,总要包含 \(1,2\) 。当 \(m < n\) ,总不包含 \(n\) 。那么只要 \(n\)\(1,2\) 中间,则最终无论如何只会存在一个 \(n\) 排列。

dls 题解:
最关键的事情是。我们可以打表找规律。遍历 \(n\)\(1\)\(8\) ,然后暴力算排列,再暴力检查,查询每个 \(n\) 在最优情况下,子区间的排列个数最少是多少。时间复杂度是 \(O(n! n^{2})\)

我们不难发现这个数总是 \(1\) 。打印这些情况,不难发现总会有 \(n\)\(1,2\) 中间。证明就是稍微归纳一下或者分析,但是打表大概率是显然对的。

因为只要你不会做,你最坏也就是手动打表,还不如用计算机打表。只要平时多练习打表怎么写代码,这件事就能很快完成。

实现:
处理出 \(pos(i)\) 数组代表数 \(i\) 在的位置。若 \(pos(n)\) 已经在 \(pos(1)\)\(pos(2)\) 中间,因为必须操作一次,交换 \(1,2\) 的位置即可。
\(pos(n)\)\(pos(1),pos(2)\) 左边,则和靠左那个位置交换。反之和靠右那个位置交换。注意下位置大小就行。

view
cin>>n;
L(i,1,n)loc[i]=-1;
L(i,1,n){
	int x;cin>>x;
	loc[x]=i;
}
l=min(loc[1],loc[2]);
r=max(loc[1],loc[2]);
if(l<loc[n]&&loc[n]<r)cout<<l<<" "<<r<<"\n";
else if(loc[n]<l)cout<<loc[n]<<" "<<l<<"\n";
else if(r<loc[n])cout<<r<<" "<<loc[n]<<"\n";
else exit(0);

6

https://codeforces.com/problemset/problem/1811/C

问:
有一个长度为 \(n\) 的非负整数数组 \(a\) 。构造一个新的长度为 \(n-1\) 的数组 \(b\) ,满足 \(b_i=max(a_i,a_{i+1}),(1 \leq i \leq n-1)\)
现在告诉你数组 \(b\) ,要求找到一个合法的 \(a\)

思考:
这题我第一次做好像十几分钟就做出来了,第二次做就完全不会了,甚至题解也没看懂。第三次做是现在,也是一点思路没有。

逆天:
我把题解一抄,交上去,过了题目,但是过不了自己捏造的样例???所以这个狗屎题,是不保证实际上所有数据都是有解的!但是它默认了给出的数据一定有解。

这题的题解是史,无论是官方题解还是网上的题解。这题分明就是一个贪心,但是官方题解的 mike 却和高斯一样先画靶再射箭,网上其他题解也基本被官方题解诱导了。

为什么会这么逆天?

如果保证给出的数据一定有解,那么就好做了。
注意到: \(a_1\) 只对 \(b_1\) 产生贡献,\(a_n\) 只对 \(b_{n-1}\) 产生贡献,所以让 \(a_1=b_1\)\(a_{n}=b_{n-1}\) 一定是不亏的。
接下来就是经典的贪心。
考虑 \(i=2\) ,若 \(a_2\) 可以是 \(b_2\) 但不让它是 \(b_2\) ,肯定是亏了的。所以说 \(a_2\) 能是 \(b_2\) 最好是 \(b_2\) ,否则是说 \(a_2\) 只能是 \(b_1\)
考虑 \(2<i<n\) ,若 \(a_i\) 可以是 \(b_i\) 则让他是 \(b_i\) 肯定是不亏的,否则只能是 \(b_{i-1}\)

具体的实现就是说,若 \(max(a_{i-1},b_{i})=b_{i-1}\) ,则 \(a_{i}=b_{i}\) ,否则只能 \(a_{i}=b_{i-1}\)

如果数据有解,那这个贪心的正确性是被证明过的。如果这个贪心的结果是错的,那么给的数据本身就是无解的。

突然又觉得这本身是个好 idea ,但是题面居然不说保证或不保证给出的数据有解,真是够恶心人的。mike 自己知道数据是否一定有解吗?mike 只知道这个贪心板子是对的。

据 mike 所说,他只靠背过的板就能上紫,复习的板够多可能能上 2400 。哈吉 mike 你居然这么无才无德吗。

总结:为什么我第一次写这题的时候这么猛,因为脑子里各种板子,都不需要考虑正确性,感觉是就直接套,一套一个对,框框上分。

view
cin>>n;
L(i,1,n-1)cin>>b[i];
a[1]=b[1];
a[n]=b[n-1];
L(i,2,n-1){
	if(max(a[i-1],b[i])==b[i-1]) a[i]=b[i];
	else a[i]=b[i-1];
}
L(i,1,n)cout<<a[i]<<" \n"[i==n];

7

https://codeforces.com/problemset/problem/1797/B

问:
给一个 \(n \times n\)\(01\) 矩阵,需要严格执行 \(k\) 次操作:

  • 选择一个格子翻转颜色。每个格子可以选择任意次。

询问最终能否使得矩阵旋转 \(180\) 度后依然一样。

\(1 \leq n \leq 10^{3}, 0 \leq k \leq 10^{9}\)

思考:
看到这个这么大的 \(k\) 就知道他在说勾吧,\(k\) 至少存在一个有意义的上界是:超出 \(n^{2}\) 的部分可以直接模掉 \(2\) 得到的 \(k\)

实际上我直接把矩阵旋转 \(180\) 度后,看两个矩阵有多少个点是不一样的。考虑一下,不管 \(n\) 的奇偶性,这样都会把这些点算两次。稍微写几个样例发现基本是对的。
得到的点除以 \(2\) 记作 \(cnt\) ,若 \(k-cnt \geq 0\) 则是有可能得到答案。

接下来才要根据 \(n\) 的奇偶性讨论。若 \(n\) 是奇数,至少接下来多余的 \(k\) 可以无脑在中点翻转,总是正确的答案。若 \(n\) 是偶数,多余的 \(k\) 只能两两抵消,这时候就需要 \(k-cnt\)\(2\) 的倍数才是答案。再否则则不难得到答案。

  • 至于矩阵怎么旋转 \(180\) 度,把横坐标和纵坐标分别对称变换一下,不难证明显然是对的。
  • 至于怎么对称,比如 \(i \in [l \sim r]\) 的一组对称是 \((i, r+l-i)\) ,显然满足 \((i+(r+l-i))/2=(r+l)/2\)
view
cin>>n>>k;
L(i,1,n)L(j,1,n)cin>>g[i][j];
cnt=0;
L(i,1,n)L(j,1,n)cnt+=g[i][j]!=g[n+1-i][n+1-j];
cnt/=2;
if(k-cnt>=0){
	if(n&1)cout<<"YES\n";
	else{
		if((k-cnt)%2==0)cout<<"YES\n";
		else cout<<"NO\n";
	}
}else{
	cout<<"NO\n";
}

8

https://codeforces.com/problemset/problem/1793/B

问:
\(n\) 个整数 \(a_1,a_2, \cdots, a_n\) 排成一个环,并且相邻两个数之差的绝对值是 \(1\) 。称 \(a_i\) 为局部最大值当且仅当 \(a_i > a_{1+(i+1-1 /bmod n)}\)\(a_i > a_{1+(i+n-1-1)/bmod n}\)
局部最小值则是小于号。现在只给出所有整数的局部最大值和局部最小值的和 \(x,y\) ,要求构造这个环,使得 \(n\) 最小。

\(-10^{9} \leq y < x \leq 10^{9}\)

思考:
首先可以找到一个下界即 \(n \geq 3\) ,否则不存在局部最大最小值的概念。
其次注意到给定的数据满足 \(y < x\) ,这里其实没分析出啥来。

比如给的样例是 \(3,-2\) ,那我可以构造 \(3,-2,1\) ,就可以满足条件。但是一个答案是 \(0, 1, 2,1, 0, -1, 0, -1, 0, 1\) ,不禁让我确信我理解错了题目。然后回头检查发现,要求 \(a_i\) 的相邻两个数之差的绝对值是 \(1\)

好。那实际上等价于我们要画一条折线图,保证这条折线的宽度是最短的,然后这条折线在循环意义下满足极大值之和为 \(x\) 极小值之和为 \(y\)

画一条折线满足只有一个极大值和极小值,应该是不亏的,如果做得到的话。

比如说样例 \(3,-2\) 。我们可以 \(3,2,1,0,-1,-2,-1,0,1,2\)

可以断言没办法继续压缩这条折线的水平位移,因为我们观察到,无论调整这条折线,那么他的极大值之和的绝对值加上极小值之和的绝对值,是不变的。这让我们知道,每类水平位移相等的折线都有一种等价性,总满足极大值之和的绝对值加上极小值之和的绝对值不变

那么实际上环长答案就是唯一的 \(2(x-y)\) ,并不需要考虑所谓的要求覆盖的 \(x\) 轴最短,也不需要考虑极小值之和小于极大值之和。

实现上我们显然可以根据二维曼哈顿距离的性质知道环长是 \(2(x-y)\) ,其次我们根据点和边的性质知道减和加的操作各执行了 \(x-y\) 次。

view
cin>>x>>y;
cout<<2*(x-y)<<"\n";
int v=x;
L(i,1,2*(x-y)){
	cout<<v<<" \n"[i==2*(x-y)];
	if(i<=x-y)v--;
	else v++;
}

然后是补充严谨证明。
观察一:考虑极大值和极小值的数量。

如果是在线性序列上,极大值和极小值的个数之差的绝对值为 \(1\) 。如果是在环上,极大值和极小值的个数之差的绝对值为 \(0\)

由于是在环上,不妨令这个数为 \(k\) 。不妨让第一个极大值比第一个极小值先出现。他们分别是 \(a_1,a_2,\cdots,a_k\)\(b_1,b_2,\cdots,b_k\)

观察二:考虑折线的竖直路程。

\[(a_1 - b_1) + (a_2 - b_1) + (a_2 - b_2) + (a_3 - b_2) + \cdots + (a_k - b_k) + (a_1 - b_k) = 2 \cdot \sum_{i=1}^{k} a_i - 2 \cdot \sum_{i=1}^{k} b_i = 2(x-y) \]

也就是说折线的竖直路程是固定的。(事实上,我们发现折线的水平位移也是固定的。)

有了这个观察,一个合法并且容易想到的构造是 \(x,x-1,\cdots,y+1,y,y+1,\cdots,x-1\)

9

https://codeforces.com/problemset/problem/1788/B

问:
给一个正整数 \(n\) ,找了个非负整数 \(x,y\) 满足:

  • \(x+y=n\) ,并且 \(x\) 的数位和与 \(y\) 的数位和之差的绝对值最多是 \(1\)

思考:
这咋构造?按位构造,然后调整一下就行了对吗。
我们从低往高遍历 \(n\) 的十进制,取出 \(\lfloor \frac{n}{2} \rfloor,\lceil \frac{n}{2} \rceil\) 。若 \(dig(x) > dig(y)\) 则让 \(x\) 乘以 \(10\) 加上 \(\lfloor \frac{n}{2} \rfoor\)\(y\) 乘以 \(10\) 加上 \(\lceil \frac{n}{2} \rceil\) 。否则就反过来。

麻烦的事情:
发现这样做的话,我们要找到 \(n\) 的最高位 \(1\) ,遍历到这个位置就要停下来,否则 \(x,y\) 会做多余的乘以 \(10\)

简单的事情:
我们有一种方法可以无脑遍历 \(k=0 \sim 9\) 。取 \(n / 10^{k} \bmod 10\) ,假设 \(x\) 要加上 \(u\) 的贡献,那么只需让 \(x\) 加上 \(10^{k} * u\)
我们可以维护 \(p=10^{0}=1\) ,每个循环 \(p*=10\)

hack:
其实是我自己写代码的时候写错了,当 \(n / 10^{k} \bmod 10\) 的时候我 continue 了,这回导致 \(p\) 的维护出错。显然不能 continue 。实际上如果 \(0\) 是空贡献就不需要处理,如果 \(0\) 存在不合法的贡献就加个 \(if\) 判断非 \(0\)

view
cin>>n;
int pw=1;
int x=0,y=0;
int sx=0,sy=0;
L(i,0,9){
	int u=(n/pw%10)/2,v=(n/pw%10+1)/2;
	if(x>y)sx+=u,sy+=v,x+=u*pw,y+=v*pw;
	else sx+=v,sy+=u,x+=v*pw,y+=u*pw;
	pw*=10;
}
if(x>y)swap(x,y);
cout<<x<<" "<<y<<"\n";

总结:
对上下取整稍微敏感一些就能知道怎么做。对数位相对敏感一些就能写出比较优雅的代码。这里其实主要是程序的实现方式要背一下板,比如我滥用 continue 很容易出问题。

10

https://codeforces.com/problemset/problem/1783/B

问:
对于一个 \(n \times n\) 的矩阵,定义他的价值是,有相邻边的两个数之差的绝对值组成的集合的大小。

现在要求构造一个 \(n \times n\) 的矩阵并且填入 \(1,2,3,\cdots,n^{2}\) ,使得这个矩阵的价值最大。

思考:
注意到每个格子只影响他周围四个同边的格子。我们或许可以归纳地构造。
然后考虑一些边界,一个显然是上界是 \(n^{2}\)
一个显然的下界是 \(2\) ,我们可以顺序填入 \(1 \sim n^{2}\) ,使得只存在 \(1,n\) 。但下界不重要。
直觉上 \(n^{2}-1,n^{2}-2\) 这种数很难出现,\(1,2\) 这种数很容易出现。换句话说越大的数越难出现。

但是上面那些都不关键啊。

通常一个好的方法是打表去找大小为 \(n\) 的集合的价值 \(f(n)\)\(f(n)\) 通常是固定的。这会让我们更容易构造。

但是,这里对于 \(2^{2}!,3^{2}!\) 已经挺麻烦了,\(4^{2}!\) 是跑一整场比赛也不可能跑出来的。

我们从 \(2^{2},3^{2}\) 的矩阵里观察到总能让代价变成 \(n^{2}-1\) ,我们需要大胆猜测这是一个价值下界,并且构造它。

实际上 \(n^{2}-1\) 是一个显然的上界。这说明如果我们总能构造出 \(n^{2}-1\) 的价值,那么我们就找到了答案。

因为这是很经典的上下界约束,所以直觉上就是这么做的。

按数从大到小,想办法构造一些情况,发现 \(3^{2},4^{2}\) 的矩阵确实能得到 \(8\)\(15\) 的价值。我们更加信任了我们的猜测。

那怎么实现一个程序构造 \(n^{2}-1\) 价值的 \(n \times n\) 矩阵呢?

想了很多方法,我没办法通过程序构造,只能手玩啊。

灵机一动:
上个厕所,我突然灵机一动,注意到对于一个相邻的 \(x,y\) ,那么 \(|x-y|\) 实际上是他的边权。
首先我们显然能发现一个显然的上界是 \(n^{2}-1\),并且我们正在构造这个上界。
其次 \(n \times n\) 的矩阵一共有 \(n \times (n-1) \times 2\) 条边,而我们要构造 \(n^{2}-1\) 条边。
一个想法突然出现了,如果直线上的序列可以构造,那么矩阵上显然可以回型构造。
那么现在我们看看直线序列能不能构造。显然容易想到 \(1,n^{2},2,n^{2}-1,\cdots,\lfloor \frac{n^{2}}{2} \rfloor,\lceil \frac{n^{2}}{2} \rceil\) ,经过验证发现确实可以构造。

实现:
首先是写一个回型填数程序,交替让 \(1\) 自增和 \(n^{2}\) 自减填入。
主要困难是回型填数,这个板我有点忘了,但是突然记起来一个 while,套五个 if else ,前四个 if else 是方向,最后一个 else 是 break,重新记一下。
交替填数可以用一个异或标记。

view
cin>>n;
flg=1;
x=1,y=1;
u=1,v=n*n;
while(true){
	vis[x][y]=1;
	if(flg)a[x][y]=u++;
	else a[x][y]=v--;
	flg^=1;
	if(y+1<=n&&!vis[x][y+1])y+=1;
	else if(x+1<=n&&!vis[x+1][y])x+=1;
	else if(y-1>=1&&!vis[x][y-1])y-=1;
	else if(x-1>=1&&!vis[x-1][y])x-=1;
	else break;
	
}
L(i,1,n)L(j,1,n)cout<<a[i][j]<<" \n"[j==n],vis[i][j]=0;

总结:
程序实现的板有点不熟悉,几年没写了,好在现在记起来了。

题目的板呢?能不能快速做题。
首先带价值的构造,找上下界然后假设,是很经典的。问题是假设之后,怎么构造。
这个构造板是这样的:如果我们能构造线性序列满足条件,那么可以回型填充矩阵使得矩阵上存在一条路径满足条件

1200 构造

1

https://codeforces.com/problemset/problem/2092/C

问:
给一个正整数数组 \(a_1, a_2, \cdot, a_n\) ,可以执行以下操作任意次(可以是 \(0\) 次):

  • 选择 \(1 \leq i \neq j \leq n\) 使得 \(2 \nmid (a_i + a_j)\)\(a_i > 0\) ,令 \(a_i\) 减少 \(1\)\(a_j\) 增加 \(1\)

询问经过若干次操作后,最大的 \(max(a_1, a_2, \cdots, a_n)\)

思考:
首先就是经典的 \(x+y\) 是奇数,这要求 \(x,y\) 的奇偶性不同。

当我们执行一个数加一,一个数减一,这只会导致一个奇数变偶数,一个偶数变奇数,奇数的数量和偶数的数量并不会改变。

话虽如此,但是我好像不知道这种题怎么做是更可控的方式。

然后思考了一下两个性质:

  • 虽然奇数的总数和偶数的总是是不变的,但是如果存在一个数的奇偶性一直改变,问题会很不可控。
  • 我们可以先让每个数在奇偶性不变的情况下处理完,然后再处理掉最后一部分。这样分布的处理就是可控的。

首先数组要存在一个偶数和一个奇数。否则不能操作,答案直接就是元素组的最大值。

如果可以操作,要想办法怎么让操作一步步都是可控的。

想到一个很可控的第一步处理,就是先不改变每个数的奇偶性。每个偶数一开始都模掉 \(2\) 变成 \(0\) ,模掉的部分然后加到任意某个奇数上。事实上我们可以只留一个奇数,然后另外的奇数都模二变成 \(1\) ,模掉的部分可以借助某个偶数,加到留下的奇数上。然后我们就得到了一个极端情况,保证最大值是奇数且不会更优。

另一个可控情况是,每个奇数一开始模掉 \(2\) 变成 \(1\) ,模掉的部分加到任意某个偶数上。我们可以只留一个偶数,另外的偶数都模 \(2\) 变成 \(0\) ,奖模掉的部分借助某个奇数加到留下的偶数上。我们有得到了一个极端情况,保证最大值是偶数且不会更优。

那么我们处理完了两种可能的答案。

我们总结上述两个情况,贡献方案是让所有数模 \(2\) ,记录模掉的贡献之和。然后将这部分贡献加到某个 \(1\) 或者 \(0\) 上。显然加到 \(1\) 上更优,且一定存在 \(1\)

于是若存在一个奇数和偶数,我们遍历 \(i\) 维护 \(sum\)\(\lfloor \frac{a_i}{2} \rfloor \cdot 2\) 的和,答案就是 \(sum+1\)
否则答案直接就是 \(max(a_1,a_2,\cdots,a_n)\)

view
cin>>n;
flg=0;
mx=0;
sum=0;
L(i,1,n){
	int x;cin>>x;
	if(x&1)sum+=x-1,flg|=1;
	else sum+=x,flg|=2;
	chkmax(mx,x);
}
if(flg==3) cout<<sum+1<<"\n";
else cout<<mx<<"\n";

总结:
你说能不能把题做对,这个其实也不是很有用。关键是遇到这种题怎么快速把题做对。
一个 trick 是,奇偶性不同的两个数之和是奇数,奇偶性相同的两个数之和是偶数。
另一个 trick 是,涉及奇偶数总数不变,但某个数奇偶性会改变的问题,可控的核心是:优先保持每个数在不改变奇偶性的情况下完成最多的操作。这可以使得,接下来的所有操作一定会改变每个数的奇偶性。这样问题就通过分部处理变得非常可控。

等等,这是非 edu 的 d2 的 t3 ?这是人能出的题啊?这是题啊?故意出成手速场?

2

https://codeforces.com/problemset/problem/2085/B

问:
给一个非负整数数组 \(a_1, a_2, \cdots, a_n\) 满足 \(n \geq 4\) 。你需要执行以下操作直到 \(|a|\) 变成 \(1\)

  • 选择 \(1 \leq l < r \leq |a|\) ,用 \(mex(a_l, a_{l+1},\cdots,a_r)\) 替换 \(a_l, a_{l+1},\cdots,a_r\)

你需要让 \(a\) 最后只剩下一个 \(0\) 。输出操作次数,并按顺序输出每次选择的 \(l,r\) 。你不需要让操作次数最小。

思考:
我绰,等一下,这个题我是不是做过?好像我去硬讨论一堆奇奇怪怪的东西然后讨论不出,然后最后灵机一动不去讨论了,乱搞十几分钟写完了。具体我也忘了我这么做的。

然而下面我又讨论出来了……

如果最后 \(a\) 变成只有一个 \(0\) ,那么倒数第一次操作,\(|a| \geq 2\)\(\forall a_i > 0/\) 。问题其实变成怎么得到这个情况。

考虑怎么让所有数都 \(>0\) 。要么是一开始就没有 \(0\) ,要么是选择大于等于二段存在 \(0\) 的区间用 \(mex\) 替换,那么这段区间就会变成一个 \(>0\) 的数。

首先你必须非常关注一个问题,我们选择的区间需要 \(\geq 2\) ,这是个比较麻烦的事情。

一个好考虑的思维板子是,枚举 \(k = 2,3,\cdots,n-2\) ,分成区间 \([1,k],[k+1,n]\) 。这两个区间都存在一个 \(0\) ,我们可以两次操作使得这两个区间分别变成一个 \(>0\) 的数,最后一次操作让这两个数变成 \(0\)

否则。
要么是只有两个 \(0\) ,且这两个 \(0\)\(1,2\) 位置,或者这两个 \(0\)\(n-1,n\) 位置。这种情况我们可以让着两个 \(0\) 直接变成 \(1\) ,然后再做一次操作就能做完。
要么是存在一个 \(0\) ,那么我让左边一个数和它作一段区间就行,否则 \(0\) 在第一位则我让右边一个数和它作一段区间。
要么没有 \(0\) ,这样直接一步操作就能到位。

草泥马。怎么给我讨论对了啊。

view
cin>>n;
L(i,1,n)cin>>a[i];
L(i,1,n)f[i]=f[i-1]+(a[i]==0),g[n+1-i]=g[n+1-i+1]+(a[n+1-i]==0);
ok=0;
L(k,2,n-2)if(f[k]>0&&g[k+1]>0){
	ok|=1;
	cout<<3<<"\n";
	cout<<1<<" "<<k<<"\n";
	cout<<2<<" "<<n-k+1<<"\n";
	cout<<1<<" "<<2<<"\n";
	break;
}
if(!ok){
	if(a[1]==0&&a[2]==0){
		cout<<2<<"\n";
		cout<<1<<" "<<2<<"\n";
		cout<<1<<" "<<n-1<<"\n";
	}else if(a[n-1]==0&&a[n]==0){
		cout<<2<<"\n";
		cout<<n-1<<" "<<n<<"\n";
		cout<<1<<" "<<n-1<<"\n";
	}else if(count(a+1,a+n+1,0)==0){
		cout<<1<<"\n";
		cout<<1<<" "<<n<<"\n";
	}else{
		if(a[1]==0){
			cout<<2<<"\n";
			cout<<1<<" "<<2<<"\n";
			cout<<1<<" "<<n-1<<"\n";
		}else{
			L(i,2,n){
				if(a[i]==0){
					cout<<2<<"\n";
					cout<<i-1<<" "<<i<<"\n";
					cout<<1<<" "<<n-1<<"\n";
					break;
				}
			}
		}
	}
}
L(i,1,n)f[i]=g[i]=0;

总结:
为什么能讨论对。

  1. 因为我就着让问题尽可能可控的原则,先套了个前后缀的板,而不是乱讨论。这样问题就变得很容易讨论。而我当时写这题的时候其实是在乱讨论。
  2. 其次是,当时我并没有很关注一件事情:考虑问题终点状态的前一个状态,因为这个状态比终点态更可控。
    • 当时隐约有这个感觉,但是一直没对这个地方集中注意力

星子哥找前后缀区间的思路和我的一模一样,江丽的考虑最前两个和最后两个数的思路和我一模一样。这说明背板是有用的。

3

https://codeforces.com/problemset/problem/2072/C

问:
给一正整数 \(n,x\) ,构造一个长度为 \(n\) 的数组满足:

  • \(a_1 | a_2 | \cdots | a_n = x\)
  • \(mex(a_1, a_2, \cdots, a_n)\) 最大。

\(1 \leq n \leq 2 \cdot 10^{5}\)\(0 \leq x < 2^{30}\)

思考:

我这题好像也做过的啊,好像很快就有思路了,但是写出了一些 bug ,我记得是向右移位的时候多移了一位。哎算了再写一遍吧。

反正说是 \(mex\) 最大,那么我们就从小往大构造。反正 \(a\) 的顺序也不关键,不妨让 \(a\) 有序。

\(a_1\)\(0\) 开始,然后是 \(a_2=1\)\(a_3=2\),……。一旦出现一个 \(a_i\) ,满足 \(a_i\) 的最高位是 \(1\) 而这一位 \(x\)\(0\) ,就可以停止程序。不考虑更低位是因为从 \(0\) 开始自增,前面的位置一定都有 \(1\) 是出现过的。

然后我们让 \(a_i, a_{i+1},\cdots,a_{n}\) 都是 \(x \oplus (a_1 | a_2 | \cdots | a_{i-1})\) 。简单验证就能发现是符合第一个条件的。

我们维护住了这个二进制的最高位是\(c\) 位,那么实际上它占据了 \(c+1\) 个位置,所以我们要让 \(x\) 从第 \(c\) 位开始的全保留,那么即最低的 \(c\) 位全变成 \(0\) 。一个快速的方法是右移 \(c\) 位再左移 \(c\) 位。

第二个条件是显然也满足的,因为我们已经没办法继续自增了。

hack:
第一个条件是不满足的。
我们的程序分两种情况:

  • \(a_1\)\(0\) 开始自增,自增到 \(a_n\) 都满足条件。
    • 可能确实有 \(a_1 | a_2 | \cdots | a_n = x\) ,那么 \(mex=n-1\) 就是最大的。
    • 那么可能会导致 \(a_1 | a_2 | \cdots | a_n \neq x\)。这时候我们需要做一次 pop_back ,也就是经典的反悔步骤。然后构造 \(a_n\) 满足 \(a_1 | a_2 | \cdots | a_n = x\) 。这可以保证 \(mex=n-2\)
  • \(a_1\)\(0\) 开始自增,自增到 \(a_k\) ,第 \(k\) 位不满组条件。
    • 那么 \(mex=k-1\) 是可以保证是最大的。而 \(a_k, a_{k+1},\cdots,a_n\) 我们可以构造,使得 \(a_1 | a_2 | \cdots | a_n = x\)

需要重点记忆的点!!!

  1. 我们不难证明 31-__builtin_clz(x) 等价于 \(\lfloor \log_{2} x \rfloor(x > 0)\)
    • 所以我们我们要把 \(x=0\)\(x>0\) 的情况分别处理,并且只有在 \(x>0\) 时才有 \(\lfloor \log_{2} x \rfloor\) 的概念。
      2.我们不难证明 \(z=\lfloor \log_{2} x \rfloor(x > 0)\) 代表 \(x\) 的最高位 \(1\) 位置。
    • 更具体地说,这个位置代表 \(2^{z} | x = 2^{z}\)
    • 更具体地说,我们讨论二进制的位置,喜欢考虑第 \(0\) 位是有效的。
  2. 实现地时候,我们先获取 \(z\) ,如果不合法则会 break ,意味着 z 是最高的不合法位置。意味着 \(0,1,\cdot,z-1\)\(z\) 个位置都是合法的。所以我们可以通过 \(x\) 左右移动截断最低的 \(z\) 个位置。
  • 非常注意,初始化 \(z=0\) 的意义,我们记录的是当前的不合法的最高位 \(1\) 。这意味着不合法的最高位 \(1\) 是第 \(0\) ,换句话说合法的情况一个 \(1\) 也没有,或者这只意味着当前 \(0\) 是合法的。
  1. 很多时候我们跑程序是希望发生一次中断的,比如此题。但这也意味着程序跑完也可能不存在中断,一个需要背的板是,如果存在这种情况:
  • 考虑是否没有发生中断也是完美答案。如果是,那就是答案。
  • 如果没有发生中断导致答案不完美。那么反悔最后一次。然后最后一步(剩下的步)按照预期中发生了中断的情况处理。

下面是代码,因为易错点很多,所以写了很多注释。

view
cin>>n>>x;
tot=0;
z=0; // 初始化 z=0 。当前不合法的最高位 1 的位置是第 0 位,这只能说明当前 0 是合法的 
L(i,1,n){
	if(i==1){ // log_{2} 0 没有意义,特殊处理 
		a[++tot]=0;
	}else{
		int y=i-1; // log_{2} y, y>0 有意义 
		z=31-__builtin_clz(y); // 第 z 位是 y 的最高位 1 位置 
		if(x>>z&1){ // x 的第 z 位如果有 1 ,那就是对的 
			a[++tot]=y;
		}else break; // 否则这个 z 就是错的,这说明 z 是最高的不合法的位置,这说明 0,1,2,...,z-1 的低 z 个位置都是合法的 
	}
}
if(tot==n){ // 如果程序直到结束也没有发生中断,但实际上我们预期发生中断。 则我们看不中断的程序是不是完美的 
	Or=0;
	L(i,1,tot)Or|=a[i];
	// 如果是完美的则不管
	// 如果不是完美的,反悔最后一步。然后让剩下的步按照预期的发生了中断的情况处理 
	if(Or!=x){
		--tot;
	}
}
x=x>>z<<z; // 把 0,1,2,...,z-1 的合法位置推平成 0 
while(tot<=n)a[++tot]=x;		
L(i,1,n)cout<<a[i]<<" \n"[i==n];

总结:
一想就会做。一写就不对。反正就是很多板没可以背,也有很多板理解的不透。最终导致代码乱写, bug 乱飞。

4

https://codeforces.com/problemset/problem/2056/C

问:
对于一个序列 \(a_1, a_2, \cdots, a_n\) ,定义 \(f(a)\)\(a\) 中的最长回文子序列长度,定义 \(g(a)\) 是长度为 \(f(a)\) 的回文子序列的数量,换句话说 \(g(a)\) 统计了 \(a\) 最终最长回文子序列的数量。

给一个正整数 \(n\) ,构造一个长度为 \(n\) 的序列 \(a\) 满足:

  • \(\forall i, 1 \leq i \leq n, 1 \leq a_i \leq n\)
  • \(g(a) > n\)

可以证明在数据范围内一定存在构造方式。

\(6 \leq n \leq 100\)

思考:
你麻痹,看了下样例就来感觉了。

首先想构造尽可能多的回文子序列,比如 \(1,2,1,2,1,\cdots\) ,但发现我们只要求最长的回文子序列,而这么做会使得最长回文子序列的个数很少。

然后想到,存在一种构造,能约束出最长回文子序列的某个下界:\(1,2,3,\cdots,n-1,1\) ,这样就有 \([1,2,1],[1,3,1],[1,4,1],\cdots,[1,n-1,1]\)\(n-2\) 个最长回文子序列。这是一定能约束到的下界。

然后感觉不太会,稍微看了下样例,发现存在一种情况。我们可以让 \(a_{n-1},a_{n}\) 都做右端点。即 \(1,2,3,\cdots,n-2,1,1\) ,这样就有双份的 \([1,2,1],[1,3,1],[1,4,1],\cdots,[1,n-2,1]\) ,然后还有一个 \([1,1,1]\)。 共 \(2(n-3)+1\) 个最长回文子序列。当 \(n > 6,2(n-3)+1>6\) 。正好是题目的数据范围。

代码其实不需要放。就是填入 \(1,2,\cdots,n-2,1,1\) 而已。

再详细分析一下:

  1. 一个左端点两个右端点,或者两个左端点一个右端点,是不是都能搞出 \(2(n-3)+1\) 个长度为 \(3\) 且最长的回文子序列?是的。
  • 有什么性质吗?肯定是双份的端点要尽可能粘合,不然没法玩。这是一个构造板:粘合端点。
  • 根据端点对区间的覆盖公式感受一下其实很有道理,具体证明应该会很无聊。
  1. 一个左端点三个右端点在这题行不行?我们至少存在一个 \([1,1,1,1]\) 的长度为 \(4\) 的最长子序列,那么和构造长度为 \(3\) 的最长子序列是相违背的。
  2. 为什么会去想构造长度为 \(3\) 的子序列?因为 \(1,2,3\) 这种东西是非常可控的,这也是一个构造板。
  • 比如 \(1\) ,如果条件允许我们可以让左右端点重合。只需要考虑一个元素。
  • 比如 \(2\) ,如果条件允许我们可以让区间只有左右两个端点。只需要考虑端点,不需要考虑中间的元素。
  • 比如 \(3\) ,如果条件允许,我们可以固定两个端点,而中间的元素只有一个。考虑完端点的情况下,只考虑一个元素,非常可控。
  • 比如 \(\geq 4\) ,考虑完端点后,中间的元素 \(\geq 2\) 个这就变得很不可控了。比如中间要两个元素,一个很大的不可控性,两个是朴素情况下需要平方的枚举。
  • 很板的是:很多构造题中通常都会考虑构造 \(1,2,3\) 长度的子序列或者子串。

5

https://codeforces.com/problemset/problem/2041/E

这尼玛怎么是亚洲的某个区域赛题目?

问:
给两个整数 \(a,b\) ,构造一个任意数组 \(c_1,c_2, \cdots, c_n\) ,使得这个数组的平均数严格是 \(a\) ,中位数严格是 \(b\)

\(-100 \leq a,b \leq 100\) 。要求 \(1 \leq n \leq 1000\)\(|c_i| \leq 10^{6}\)

这里说明一下通常的平均数和中位数的定义(确实有点忘了……hhh)。
平均数:\(\frac{1}{n} \sum_{i=1}^{n} c_i\)
中位数:将数组排序后,\(\frac{1}{2}(c_{\lfloor n/2 \rfloor}+c_{\lceil n/2 \rceil})\)

思考:
怎么让平均数是 \(a\) 呢。那我是不是随便想一个 \(a,a,\cdots,a\) 就好了。
怎么让平均数是 \(a\) 的情况下中位数是 \(b\) 呢?发现先用平均数构造数组,再调整数组构造中位数不可控。
那么感觉先用中位数构造数组,再调整构造平均数可控。

比如先构造中间 \(b\) ,那么中位数显然。接下来构造两边,只需要 \(b+2 \cdot k \cdot x = (2 \cdot k+1) \cdot a\)
或者开始构造中间 \(b,b\) 。接下来构造两边,需要 \(2b+2 \cdot k \cdot x = (2 \cdot k+2) \cdot a\)

第一种情况是 \(b-a=2 \cdot k \cdot(a-x)\)\(x=\frac{a-b}{2 k} + a\) ,当且仅当找到一个 \(k\) 满足 \(2 k \mid a-b\) 有解。
第二章情况是 \(2b-2a=2 \cdot k \cdot(a-x)\)\(x=\frac{2a-2b}{2 k} + a\) ,当且仅当找到一个 \(k\) 满足 \(2 k \mid 2a-2b\) 有解。

发现第二种情况更具有一般性,因为 \(2a-2b\) 总是一个偶数。那不妨就让 \(k=\frac{2a-2b}{2}\) ,则总有解。
所以我们考虑 \(b,b\) ,然后左边和右边各填入 \(k\)\(x\)
最后依照惯例检验一下特殊情况,\(b=a\) ,那么 \(b,b\) 是满足中位数是 \(b\) ,平均数是 \(2b/2=2a/2=a\)
然后检验一下数据限制。上界 \(2+\frac{2a-2b}{2} \leq 1000\) 没问题,\(|x=\frac{2a-2b}{2 k} + a| \leq 10^{6}\) 没问题。

hack1:

  1. \(2a-2b\) 是负偶数,固然 \(2k | (2a-2b)\) 是有解。但是我们需要 \(k \geq 0\)
    • 那么实际上,我们需要存在一个 \(q \geq 0\) ,使得 \(2kq=2a-2b\) 。即需要构造 \(q\) ,使得 \(k=\frac{2a-2b}{q} \geq 0\)
    • 我们之前是不妨让 \(q=1\) 。那么现在若 \(k<0\) ,不妨让 \(q=-1\) ,则 \(k=-k > 0 \geq 0\)
  2. 我们之前不妨让 \(k=\frac{2a-2b}{2}\) ,那么我直接以为 \(x=\frac{2a-2b}{2k}+a=a\) 了,我以为 \(\frac{2a-2b}{2k}\) 没有贡献了。
    • 其实乘除法里的没有贡献实际上是 \(1\) ,那么实际上 \(x=1+a\)
    • 假设执行了 \(k=-k\) ,又要更新 \(x=-1+a\) 。另一种方法是在最后老老实实计算 \(x=\frac{2a-2b}{2k}+a\)

总结1:
我从 六点一十四看题开始,到六点五十四才算完,算力看来又要重新练习了。这些算力题看来要多复习。

为什么算力不对劲。因为中途发现自己对各种定义不熟悉,各种参数忘了导致公式乱写,各种公式简写导致导出了的参数和公式又忘了。循环套下去,debuff 一层一层叠。

hack2:
看来我对中位数没有一点认知。\(x,\cdots,x,b,b,x,\cdots,x\) 。中位数看起来很像是 \(b\) 没错吧!但是你是否记得中位数要先排序呢?

一个可行的方法是,我们要继续讨论 \(x\)\(b\) 的大小关系。以为不能保证他们的正负性。

  • 假设 \(x \geq b\) ,那么让左边的 \(x\) 减去 \(x-b\)\(b\) ,右边的 \(x\) 加上 \(x-b\)\(2x-b\) 。则数组满足 \(b \leq b,2x-b \geq b\) 于是有序,中位数直接是 \(b\) 。另一方面没有改变数组和,所以平均数不受影响。
  • 假设 \(x < b\) ,那么让左边的 \(x\) 减去 \(b-x\)\(2x-b\) ,右边的 \(x\) 加上 \(b-x\)\(b\) 。则数组满足 \(2x-b < b,b \geq b\) 于是有序。
view
cin>>a>>b;
int k=(2*a-2*b)/2,x=1+a;
if(k<0)k*=-1,x=-1+a;
VI ans;
if(x>=b){
	L(i,1,k)ans.psb(b);
	L(i,1,2)ans.psb(b);
	L(i,1,k)ans.psb(2*x-b);
}else{
	L(i,1,k)ans.psb(2*x-b);
	L(i,1,2)ans.psb(b);
	L(i,1,k)ans.psb(b);
}
int n=SZ(ans);
cout<<n<<"\n";
L(i,0,n-1)cout<<ans[i]<<" \n"[i==n-1];

总结2:

心态和行为上。

想完这些的时候,起点零七分,代码 ac 了。重新整理思路加写代码看来又用了十五分钟。
其实不能妄自菲薄,因为很多时候我都在写文字,花费了很多时间。
可以注意到我的文字一直在该,说明我思路一直在改。但是科学的方法来说,经过大量 vp 后,思路可以很快,也可以修改得很快。

题目本身。

其实这种构造,更偏向数学构造,还是挺有意思的。我应该多复习这种数学构造的题目,多重视定义,不然很容易忽视定义最后发现中间错了,又回去改。

定义问题。

构造一个 \(k\) 使得 \(2k | (2a - 2b)\) 有解这种说法其实是不严谨的,是偷懒的,是现在偷懒,具体实现的时候要再做一次转化的。

  • 经常因为这一步转化没有重视,导致很多时候参数乱飞,完全忘了:
    • 哪个参数是哪个意思。
    • 哪个简写式导出了哪些参数。
    • 哪个公式原来代表哪个意思……参数都不知道是哪个意思,怎么能知道带着这个参数的公式是啥意思……
    • 各个参数的范围约束是啥……参数都不知道是哪个意思,怎么能知道它原本意思的范围约束……
  • 核心就是不能偷懒,重视定义,严谨写下参数。

实际上应该考虑成:构造一个 \(k \geq 0\) ,使得 \(2kq=2a-2b\)\(q\) 有解(应该写成“有整数解”)。

  • 上面这句话还不够严谨。让 \(q\) 有解并不本质,我们甚至不关心 \(q\) 的解是啥。
    • 我们很多时候默认所谓的有解其实是有整数解,其实又是在偷懒,是在加重后序工作的负担。
  • 更本质的是,我们需要让 \(q\) 有整数解,那么这会意味着 \(k\) 是合法的。

重新整理思路:
因为思路一开始是错的,然后我后面发现错的思路还能改成对的。所以要重新整理一个相对正确的思路。

我们还是考虑先构造中位数,然后左边构造 \(k\)\(x\) ,右边构造 \(k\)\(y\) ,保证 \(x+y=2z\) ,那么我们只需要关注 \(z\) 怎么算即可,这是非常可控的。比如构造 \(x,\cdots,x,b,b,y,\cdots,y\) 。那么平均数就可以考虑成 \((2k+2) \cdot a = 2kz+2b\) 。接下来就是一步步设元和计算了,和上面的方法一样。

一个相对优秀的设元方式: 设元 \(x+y=z\) ,然后把 \(x,y\) 放到两块位置,只去考虑 \(z\) 是一个很好的方式。如果 \(z\) 能算出来,那么 \(x,y\) 也会很容易构造。

6

https://codeforces.com/problemset/problem/2003/C

问:
给一个长度为 \(n\) 的小写字符串 \(s\) 。定义一个对 \((i,j)(1 \leq i < j \leq n)\) 是愉快对当且仅当存在一个 \(k\) 满足 \(i \leq k < j\) 并且以下两点都满足:

  • \(s_k \neq s_{k+1}\)
  • \(s_k \neq s_i\) 或者 \(s_{k+1} \neq s_j\)

定义一个对 \((i,j)(1 \leq i < j \leq n)\) 好对当且仅当 \(s_i = s_j\) 或者 \((i,j)\) 是愉快对。

你需要重排 \(s\) ,使得 \(s\) 中的好对最多。

思考:
你妈的傻逼阅读理解。重新翻译一下,一个好对 \((i,j)(1 \leq i < j \leq n)\) 当且仅当:

  • 或者 \(s_i = s_j\)
  • 或者存在一个 \(k\) 满足 \(i \leq k < j\)\(s_k \neq s_{k+1}\) ,且或者 \(s_i \neq s_k\) ,或者 \(s_{k+1} \neq s_j\)

需要重排 \(s\) ,使得 \(s\) 中的好对最多。

感觉应该还是很傻逼的脑筋急转弯。但没有看出很显然能套的板子或者 trick 。

然后那种上下界也不是很容易找,基本没法找,看不出来好对的上下界。

很新颖的题目,无所谓我会背板。背板背板背板。

看了一眼题解后的思考:
先别关注答案是什么形式,我们对任意一个 \(s\) 进行分析。
一个需要背板的思考方式是:我们把 \(s\) 分成 \([l_1,r_1],[l_2,r_2],\cdots,[l_k,r_k]\) ,使得连续的每一段都是极大的字符相同的子段。
显然每个段内任意两个 \(i,j\) 都是 \(good\) 的。
现在去考虑不同段,左边某个段选 \(i\) ,右边某个段选 \(j\) 。要存在一个 \(k=i,i+1,\cdots,j-1\) 满足愉快的条件,这个 \(k\) 只能选在一个段的右端点上。

当左右端点所在的两个段相对远的时候,\(k\) 无论选在哪个右端点都能让 \((i,j)\) 是愉快的。
当且仅当 \(i,j\) 在相邻两个段上,\(k\) 只能选 \(i\) 所在段的右端点。但是 \(s_k=s_i\)\(s_{k+1}=s_j\) ,这就没办法让 \((i,j)\) 合法了。

我们算一下贡献,先算所有大小 \(\geq 2\) 的子区间,是 \(\binom{n}{2}=\frac{n(n+1)}{2}\) 。然后减去所有左右端点在相邻两段的情况 \(\sum_{i=1}^{k-1} (r_i - l_i + 1) \times (r_{i+1} - l_{i+1} + 1)\) 。那么能得到的好的对就是 \(\frac{n(n+1)}{2} - \sum_{i=1}^{k-1} (r_i - l_i + 1) \times (r_{i+1} - l_{i+1} + 1)\) 个。

现在问题变成了,我们要重排 \(s\) ,使得 \(\sum_{i=1}^{k-1} (r_i - l_i + 1) \times (r_{i+1} - l_{i+1} + 1)\) 尽可能小。

换句话说就是,重排 \(s\) ,按相同字母分段,枚举每个段,让这个段的长度乘以右边一个段的长度,然后对这些数值求和。最终让这个东西尽可能小。

假设我们可以做到所有段大小都是 \(1\) ,也就是总可以让字母交替出现,那么我们能约束出一个上界 \(\sum_{i=1}^{n-1} 1 \times 1 = n-1\)

然后又因为显然这个东西也是下界,所以答案就是 \(n-1\) 。应该是比较显然的,因为我们发现最优的情况就是每个段大小为 \(1\) 。如果分更多的段,让某些段更大,那么最后的答案会 \(>n-1\) 。尝试调整一下就可以大概验证,比如 \(n-3 + 1 \times 2 = n-1\) 没有更优,\(n-4 + 2 \times 2 = n\) 已经更劣。

虽然不是很有道理,但是每个段大小为 \(1\) 也是个常见的板。

另一种情况是,我们没办法做到所有段大小都是 \(1\)
换句话说,根据一些神秘定理,比如摩尔投票的推论,若最大的字符数量 \(\leq \lfloor \frac{n}{2} \rfloor\) ,那我总是可以让每个元素两两配对。如果元素可以两两配对,那存在一种显然的构造方式让每个元素不相邻,比如让每个配对的对都是小元素在左边大元素在右边。

而若最大字符数量 \(> \lfloor \frac{n}{2} \rfloor\) ,那么根据鸽笼原理,我们没办法让两个元素不相邻。也就是说存在一段大小 \(\geq 2\)

再根据一些神秘结论,单独拿出一段最大的,比起分配成几段相对大的,让所有段的长度乘起来,前者会更小。这个结论在段数为 \(2\) 的时候相对容易证明。

那么现在是实现。

  • 第二种情况我们先填入最大个数的字符,然后补一个其他字符,最后的一段后缀只剩下最大个数的字符。
  • 第一种根据摩尔投票,这个构造一定能实现。但是构造方式呢?一时半会想不到。换句话说我脑子里完全没有工具。

不玩了,我去看看题解怎么构造。

从让 \(\sum_{i=1}^{k-1} (r_i - l_i + 1) \times (r_{i+1} - l_{i+1} + 1)\) 为止都是对的。接下来都是错的。

第一步,题解说明了,当 \(s\) 只有一种情况的时候需要特殊处理

第二步,题解说可以归纳证明存在一个下界,\(\sum_{i=1}^{k-1} (r_i - l_i + 1) \times (r_{i+1} - l_{i+1} + 1) \geq n-1\) 。但题解没有给出归纳证明方法。

我们猜测这个下界总可以取到,然后去构造这个下界。

第三步,题解说明了构造方法。但没有证明构造的正确性。
\(s\) 只有一种字符,不需要排序,直接输出即可。否则就是构造。
我们只需根据字符出现次数从小到大排序,然后轮流填入即可,如果字符已经为空则跳过。最终能得到,前面所有连续字符段的大小都是 \(1\) ,最后一段连续字符段的大小是一整段后缀。

实现注意事项(要背板的地方):

  1. 我们构造答案串 \(ans\) 一开始为空串。然后直到 \(SZ(ans)<n\) 都跑 while 循环,然后遍历 \(0 \sim 25\) 轮流填入还有余的字符。

  2. 排序用位置置换数组简直不要太香,注意令 \(x=p(i)\) 就行,实际上 \(i\) 这个位置对应的下标是 \(x\)

时间复杂度最坏是 \(O(n \Sigma)\) ,看作一个 \(26\) 的常数就行。精细一些可以做到 \(O(n)\)

view
cin>>n;
cin>>s;
L(i,0,25){
	cnt[i]=0;
	p[i]=i;	
}
L(i,0,n-1){
	cnt[s[i]-'a']++;
}
sort(p,p+26,[&](int i,int j){
	return cnt[i]<cnt[j];
});
ans="";
while(SZ(ans)<n){
	L(i,0,25){
		int x=p[i];
		if(!cnt[x])continue;
		ans+=x+'a';
		cnt[x]--;
	}	
}
cout<<ans<<"\n";

补充证明:

下界的不太严谨证明。
有一个经典结论。就是 \(n\) 个空位分组,让每组尽量小,可以让所有组的乘积最小。一个常见的问题是分成 \(\geq 2\) 的组,那我们就尽量分组成全是 \(2\) ,可能存在一个 \(3\) 的形式。这个证明可以归纳。

类似的问题,我们都可以想到,尽量让每组尽量小是一个优秀的方法,然后跳过一些乱归纳或者乱调整证明是对的。

另一个经典结论,一个相当大的区间的乘法贡献,比这个区间拆成若干个大小 \(\geq 2\) 的区间的乘法贡献来得远远小。在很多情况下我们归纳一下能发现他能取到最小贡献。
这里我们如果没办法使得每一段单独是一个数,或者说另一个角度来看没办法使得所有颜色都是交替出现,那么总会有且仅有一种颜色多出来,我们最好让多出来的这些颜色单独归类到一段。

在这个题中,我们稍微分析一下能发现,把这个大段放到最前面或者最后面,只会贡献一次,否则都会贡献两次,所以这时候的贡献最小。

构造的相对严谨证明。
排序,然后循环交替,最后显然能得到一个模式串,使得边界上的一段是连续的同色,而其他位置全是交替出现的颜色(每个颜色自成一段)。

总结:
为啥大家好像都挺会做这种题?我觉得这个题放在 \(d2T3\) 这个位置是相当合理的,按理说应该是 \(1600 \sim 1800\) 左右的评分。但事实上应该是很多人都做出来了,以至于评分只有 \(1200\) 。可能是这个结论大家不久前都见到过,而这种构造对大家来说见得也相对不少。

主要这题的证明还算相对好证。也确实用到了一个经典结论。总结一下该背的板吧。

  • 字符串分析的一种划分板子。按极大同色字符划分字符串。
  • 经典结论1。让每段尽可能小。
  • 经典结论2。如果存在一种颜色要划分,那么尽可能让它不要划分,或者能划分出尽可能小的段,能够对乘法造成最小的贡献。
  • 构造方式。排序,然后循环交替填入。

补充思路:
这是在完全看题解之前,我自己想到最后一刻的时候,再往后补充的思路。

两种情况稍微分析一下,都能发现能取到 \(n-1\)
第一种情况显然是所有颜色交替,显然是 \((n-1) \times 1 \times 1\)
最后一种情况下,边界上一段长度为 \(k\) 的同色段。

  • 如果只有一种颜色,那只有这一段了,没得玩。
  • 如果不是只有一种颜色,那么要加上剩下 \(n-k\) 个位置的交替颜色。那么实际上贡献是 \(k \times 1 + n-k-1 = k-1\)

注意。如果我敢对这段单独的同色段进行分析并设元算贡献,那么能发现 \(n-1\) 是总能被取到的,那么我会转而去想,当字符颜色数大于 \(1\) 时一定存在一个通用的构造方式,而不需要分类讨论,使得下界 \(n-1\) 总能被构造出来。

那么我独立思考做出这题的概率就会大很多。

7

https://codeforces.com/problemset/problem/1990/B

问:
对于一个数组 \(b_1, b_2, \cdots, b_m\) ,定义

  • \(b\) 的最大前缀位 \(i\) 是一个最小的 \(i\) 满足 \(\sum_{l=1}^{i} b_l = max_{j=1}^{m} \sum_{k=1}^{j} b_k\)
  • \(b\) 的最大后缀位 \(i\) 是一个最大的 \(i\) 满足 \(\sum_{l=i}^{m} b_l = max_{j=1}^{m} \sum_{k=j}^{m} b_k\)

给三个正整数 \(n,x,y\) ,且 \(x > y\) 。构造 \(a_1, a_2, \cdots, a_n\) 满足:

  • \(\forall i, 1 \leq i \leq n, a_i\) 要么是 \(1\) 要么是 \(-1\)
  • \(a\) 的最大前缀位是 \(x\)
  • \(a\) 的最大后缀位是 \(y\)

可以证明总存在一种构造可以满足条件。

思考:
感觉不会做,然后看了一眼题解。
注意到 \(y < x\) ,而 \(pre_{x}\) 是第一个前缀最大值,\(suf_y\) 是第一个后缀最大值。
\(x\) 取到前缀最大值,从 \(x+1\) 往后,我们从前往后按顺序构造 \(-1,1,-1,1,\cdots\) 就可以保证再往后的前缀不会更大。
\(y\) 取到后缀最大值,从 \(y-1\) 往前,我们从后往前按顺序构造 \(-1,1,-1,1,\cdots\) 就可以保证再往前的前缀不会更大。
那么只剩下 \(y,y+1,\cdots,x\) 这一段还要构造。
因为 \(x-y+1 \geq 3\) ,我们构造 \(1,1,\cdots,1\) 。总可以使得这一段的区间和 \(\geq 3\) 。而左右两段的区间和要么最大值是 \(1\) 要么是 \(0\)

感觉是板。

  • 两边算空贡献,或者维护一个平衡尽量让贡献更小。
  • 中间尽量让贡献更大。

但是对于 \(x \leq y\) 的数据,则这个板没用。

这个题基本上很像脑筋急转弯类的问题,必须得背一下。

  • 什么情况下可以背题,就是证明容易,但是 trick 难想的题,甚至只适用于一些特殊情况。很可能是我们缺乏了一些典型事例或者典型思维,按自己的理解没办法想出 trick ,所以背 trick 。
  • 什么情况下不能背题,就是你甚至无法证明这个题在这个情况下,这个 trick 是对的,要么死磕证明再背,要么放弃这题。
  • 刷题本身就是靠先证,能理解底层就先理解底层然后再背,不能理解底层就直接背。
    • 归根结底要先证。
    • 背不能纯背,背完了还得反复练,刻画肌肉记忆。
view
cin>>n>>x>>y;
R(i,y-1,1)a[i]=(y-1+i)%2==0?-1:1;
L(i,x+1,n)a[i]=(x+1+i)%2==0?-1:1;
L(i,y,x)a[i]=1;
L(i,1,n)cout<<a[i]<<" \n"[i==n];

8

https://codeforces.com/problemset/problem/1983/B

问:
给两个矩阵 \(a,b\) ,大小都是 \(n \times m\) 的,只包含 \(0,1,2\) 三种元素。
你可以对矩阵 \(a\) 进行如下操作任意次(可以是 \(0\) 次):

  • 选择一个长和宽都 \(\geq 2\) 的子矩阵。这个子矩形不必是真子矩形,你可以选择整个原矩形。
  • 这个子矩形有四个角度。选择一条对角线上的两个角 \(+1\)\(\bmod 3\) ,另一条对角线上的两个角 \(+2\)\(\bmod 3\)

通过有限次操作,是否可以把 \(a\) 变成 \(b\)

思考:

首先,差点骂人了。读到后面才发现是只操作子矩形上的四个角而不是对角线,文学没学好吗出题人。

一个经典的第一步操作就是,让 \(a\) 减去 \(b\) 然后 \(\bmod 3\) ,问题就变成了是否能经过有限次操作让 \(a\) 变成 \(0\) 矩阵。

然后好像就不是很会做了……我是真不会这些 trick 。感觉很像洛克王国幽暗空间里面的那个贪心游戏你知道吗。

现在是看一眼题解后的思考:

太牛逼了,这还真是个贪心题。而且看一眼提示后发现很典。

先写再说,下面是代码。

view
cin>>n>>m;
L(i,1,n){
	string s;cin>>s;
	L(j,1,m){
		int x=s[j-1]-'a';
		a[i][j]+=x;
	}
}
L(i,1,n){
	string s;cin>>s;
	L(j,1,m){
		int x=s[j-1]-'a';
		a[i][j]-=x;
		a[i][j]=(a[i][j]+3)%3;
	}
}
auto add=[&](int &x,int y)->void{ x=(x+y)%3; };
L(i,1,n-1)L(j,1,m-1){
	int u=2,v=1;
	if(a[i][j]==0)continue;
	if(a[i][j]==2){
		swap(u,v);
	}
	add(a[i][j],u);
	add(a[i+1][j+1],u);
	add(a[i+1][j],v);
	add(a[i][j+1],v);
}	
int ok=1;
L(i,1,n)L(j,1,m)ok&=a[i][j]==0;
cout<<(ok?"YES":"NO")<<"\n";
L(i,1,n)L(j,1,m)a[i][j]=0;

这期间出现了两个 bug 。

  • 加再模的时候下标打错了,类似 f=(f+x)%m ,f 的下标第二次再写过去容易错。所以我大概懂了为什么大家非常喜欢封装模意义下的加法,现在学到了。
  • dls 教的:单测是对的,多测是错的,那肯定是多测没清空。学会了,用上了。

然后是为什么这么写是对的。

首先是,\(a \rightarrow b\) ,两边减去 \(b\) ,则是 \(a-b \rightarrow 0\) ,所以第一次步处理很经典。
其次,我们注意到对一个 \(2 \times 2\) 的矩阵加上

\[\begin{matrix} 1 & 2 \\ 2 & 1 \\ \end{matrix} \]

可以通过相邻位置抵消,一直在横向扩展

\[\begin{matrix} 1 & 2 \\ 2 & 1 \\ \end{matrix} \rightarrow \begin{matrix} 1 & 2+1 & 2 \\ 2 & 1+2 & 1 \\ \end{matrix} = \begin{matrix} 1 & 0 & 2 \\ 2 & 0 & 1 \\ \end{matrix} \]

纵向一样是可以通过相邻位置抵消去扩展的。

这意味着:任意一个需要在主对角加上 \(2\) ,副对角加上 \(1\) 的子矩形,都能通过连续的 \(2 \times 2\) 的子矩形做这种操作得到。

我们不难证明另一种情况:任意一个需要在主对角加上 \(1\) ,副对角加上 \(2\) 的子矩形,都能通过连续的 \(2 \times 2\) 的子矩形做这种操作得到。

如果最终存在合法答案,我们把对所有相对大子矩形的直接操作数均摊到 \(2 \times 2\) 的子矩形上。
每个 \(2 \times 2\) 的子矩形在被均摊操作后,实际上要做的操作应该是相对少的,因为至少一种操作做 \(3\) 次后是会完全抵消的,显然 \(\times 3 \bmod 3 = 0\) ,贡献为空。
那么稍微细致分析一下,我们研究一下两种操作的封闭加法群,每个 \(2 \times 2\) 的子矩形在被均摊操作后,需要做的只会被均摊成以下 \(3\) 种情况之一。

\[\left [ \begin{matrix} 0 & 0 \\ 0 & 0 \\ \end{matrix} \right ] \quad \left [ \begin{matrix} 1 & 2 \\ 2 & 1 \\ \end{matrix} \right ] \quad \left [ \begin{matrix} 2 & 1 \\ 1 & 2 \\ \end{matrix} \right ] \]

所以我们遍历 \(1 \leq i \leq n-1, 1 \leq j \leq m-1\) ,贪心地去操作,最后检查整个 \(n \times m\) 矩形是不是都变成了 \(0\) 就好了。

总结:

套了些奇怪东西的贪心板。这个板子倒是很经典。套的奇怪东西其实也不难证明,但是没见过的话一时半会其实不容易直接想到,只能慢慢观察。

那么这个题其实还是挺不错的题,其实很有背一背的必要。

最后说一下,真的很像洛克王国幽暗空间里的那个贪心游戏。感觉底层原理几乎是一模一样的。

9

https://codeforces.com/problemset/problem/1979/C

问:
你要对 \(n\) 场游戏下注,如果第 \(i\) 场游戏胜利了,你能获得你所下注资金的 \(k_i\) 倍,否则你不能获得奖励。\(n\) 场游戏有且仅有一场会胜利,其他都会失败。

要求你构造一个数组 \(a_1, a_2, \cdots, a_n\) ,代表对第 \(i\) 场游戏的下注资金。满足无论哪场游戏胜利,你最终的净利润都严格大于 \(0\) 。如果不存在这种下注方式,则输出 \(-1\)

\(1 \leq n \leq 50\)\(2 \leq k_i \leq 20\)

思考:
假设存在一个合法构造,那么 \(a_1, a_2, \cdots, a_n\) 满足什么条件代表它是合法的?

\(S=\sum_{i=1}^{n} a_i\) ,则 \(S < min_{i=1}^{n} k_i \times a_i\) 当且仅当 \(a\) 是合法的。

假设我们知道一个合法的 \(S\) ,我们只需逐个分配,让最小的 \(a_i\) 满足 \(a_i \times k_i > S\) ,剩下的 \(S\) 会尽可能多,后面也会更容易构造。这是一个经典的贪心构造。

那么 \(S\) 具有单调性吗?没有。那么他要枚举?大概率是错的。于是我就不会做了。

看了一眼题解后的思考:
上面那个思路显然是对的,但是往后做不好做。

接下来的思路,是高中化学题型种常见的解题思路。

我们先假设非整数的情况也是可以考虑的,再不妨设我们最终能赚取到的纯利润是 \(1\)
我们可以让 \(a_i=\frac{1}{k_i}\) ,这就保证了无论哪个游戏赢,比如是第 \(j\) 个,最后总是能得到 \(\frac{1}{k_j} \times k_j = 1\) 的纯利润。

显然支出是 \(\sum_{i=1}^{n} \frac{1}{k_i}\) ,显然我们只需要让支出严格小于纯利润,于是有 \(\sum_{i=1}^{n} \frac{1}{k_i} < 1\)

现在考虑整数情况,怎么搞呢?我们构造一个 \(X\) 代表我们最终得到的纯利润,把上述不等式两边乘上,于是得到:

\[\sum_{i=1}^{n} \frac{X}{k_i} < X \]

只需要构造 \(X\) 满足 \(\forall \frac{X}{k_i}\) 都有正整数解即可。那么这里显然不妨是 \(S=lc=[k_1, k_2, \cdots, k_n]\)

然后稍微验证一下,发现最小公倍数最坏是 \(2 \sim 20\) 的最小公倍数,让他们的唯一分解幂次取 \(max\) 不是很大,至少不会爆 \(i64\)

注意,若 \(\sum_{i=1}^{n} \frac{lc}{k_i} \geq lc\) ,则无解。否则输出 \(i=1,2,\cdots,n\) 时的 \(\frac{lc}{k_i}\) 就做出来了。

view
cin>>n;
lc=1;
L(i,1,n)cin>>k[i],lc=lcm(lc,k[i]);
X=0;
L(i,1,n)X+=lc/k[i];
if(X>=lc)cout<<-1<<"\n";
else{
	L(i,1,n){
		cout<<lc/k[i]<<" \n"[i==n];
	}
}

时间复杂度 \(O(n \log n)\) ,但至少是可算的时间复杂度。即使 \(n \leq 50\) 好像也没有其他很好的多项式做法。

总结:
这个正解其实要证是好证的,而且显然死去的记忆又出现了,高中化学题很多时候都会用这个技巧,但我从来不背也不长记性。我想的第一个思路虽然不能说是错的,但不好解题。所以这个题的正解思路要好好背一背。

10

https://codeforces.com/problemset/problem/1935/B

问:
给一个长度为 \(n\) 的数组 \(a_1, a_2, \cdots, a_n\) 。要求你对他分成 \(k(2 \leq k \leq n)\) 段,使得每一段的 \(mex\) 都一样。或者说这是不可能的。
如果可以,输出具体的分段方案。

\(2 \leq k \leq \leq n \leq 10^{5}\)\(0 \leq a_i < n\)

思考:
这里 \(a_i < n\) ,所以没必要特殊处理,虽然 \(\geq n\) 的部分肯定是对 \(mex\) 没有贡献的。

那真要做的时候,肯定先看全局,从小到大考虑 \(mex\) 能取的数。

全局的 \(0\) 但凡存在,就一定存在一个段的 \(mex > 0\) ,所以 \(0\) 不能是所有段的 \(mex\)

  • 如果不存在 \(0\) ,随便分两段就能让两段的 \(mex=0\)
  • 如果存在 \(0\) ,若仅存在一个 \(0\) ,显然没办法让 \(\geq 2\)\(mex>0\) 。则没有答案。
  • 如果存在 \(\geq 2\)\(0\) ,再做考虑。

然后又你妈的特别搞啊,然后我又不会了。

看一眼题解后的思考:

注意一个段,如果在边上增添一个元素,要么 \(mex\) 不变,要么 \(mex\) 增加 \(1\)
也就是对于所有前后缀,\(mex\) 都是递增地单调的。
若我们选择一个分界线 \(k\) ,划分成一段前缀一段后缀 \([1,k],[k+1,n]\)

  • \(k\) 左移,这段前缀的 \(mex\) 是递减趋势,后缀的 \(mex\) 是递增趋势。
  • \(k\) 右移,这段前缀的 \(mex\) 是递增趋势,后缀的 \(mex\) 是递减趋势。

只要 \(a \neq b\) ,不妨 \(a > b\) ,我们总可以通过减少 \(a\) 增加 \(b\) 以试图让 \(a=b\)
唯一一个反例是 \(a=b+1\) ,执行一步操作后 \(b=a+1\) ,这个反例是可以存在的。

如果能证明如果两段不能划分,那么 \(\geq 3\) 段也不能划分,那么我们就能得到答案。然后脑子抽了,不太确信能不能通过合并段证明。

然后问了一下其他人,这个东西是可以做到的。然后我在知道自己没问题后继续往后考虑。

如果 \(\geq 2\) 的段 \(mex\) 都相同,意味着这些段至少 \(mex\) 都没出现过,即使我们合并任意一些段,他们合并后的 \(mex\) 依然不变。于是我们只需要考虑两段的情况。

其实这个证明还是挺关键的,是这个题的核心 trick 。

于是我们暴力维护一段前后缀去做就行了,然后前后缀的 \(mex\) 计算可以均摊成 \(O(n)\) ,找分界线的时间是 \(O(n)\) 的。

view
cin>>n;
L(i,1,n)cin>>a[i];
mexr=0,mexl=0;
L(i,0,n)vl[i]=0,vr[i]=0; // 这里要从 0 清空到 n ,并非清空 a_i ,也并非从 1 清空到 n 
R(i,n,1){
	vr[a[i]]++;
	while(mexr<n&&vr[mexr])mexr++;
}
k=-1;
L(i,1,n){
	vl[a[i]]++;
	while(mexl<n&&vl[mexl])mexl++;
	vr[a[i]]--;
	if(vr[a[i]]==0&&mexr>a[i])mexr=a[i];
	if(mexl==mexr){
		k=i;
		break; // 不 break 也不影响时间复杂度,但是 break 方便调试
	}
}
if(k==-1)cout<<-1<<"\n";
else{
	cout<<2<<"\n";
	cout<<1<<" "<<k<<"\n";
	cout<<k+1<<" "<<n<<"\n";
}

总结整理:
虽然我前面是一开始套了个 \(k=2\) 的前后缀的板,后面才补票证明了 \(k>2\) 的情况可以合并成 \(k=2\) 的情况。

但我觉得这题更科学的做法是,先观察到 \(k>2\) 的情况可以合并到 \(k=2\) 的情况,这时候即使不需要枚举板,自然而然也有很合理的前后缀思路。

其次是前后缀分别处理,不管值域怎么样,我们都只需要多测的时候把计数数组 \(v\)\(0\) 清空到 \(n-1\) ,因为 \(\geq n\) 是不可能出现的,而且这里不会对复杂度有额外贡献。

再其次是,区间变大的时候 \(mex\) 的变化,只需要 while mex < n 并且 \(v(mex) \neq 0\)\(mex\) 自增就行。正确性显然,时间均摊是 \(O(n)\) 的。
而区间变小的时候 \(mex\) 的变化,注意一旦出现 \(v(x)>0 \rightarrow v(x)=0\) ,那么 \(chkmin(mex,x)\) 即可,单次是 \(O(1)\) ,最坏会出现 \(O(n)\) 次操作,时间也是 \(O(n)\) 的。

11

https://codeforces.com/problemset/problem/1933/D

问:
给一个 \(a_1, a_2, \cdots, a_n\) ,询问是否能重排成 \(b_1, b_2, \cdots, b_n\) ,使得 \(b_1 \bmod b_2 \bmod \cdots \bmod b_n \neq 0\)

只需要回答能否做到,不需要真的构造。

\(2 \leq n \leq 10^{5}\)\(1 \leq a_i \leq 10^{9}\)\(\sum n \leq 2 \cdot 10^{5}\)

思考:
模运算有交换律吗?好像没有吧。不确定。但随便举几个例子又好像有。很不熟悉的运算。

然后比较显然的是,若有 \(x < y\) ,则 \(x \bmod y = x\) 。显然如果每个数都不同,则可以排成严格有序的情况,最后的答案就是 \(b_1 \neq 0\)

如果存在两个数相同,不妨是 \(x\) ,至少这两个 \(x\) 不能相邻。还有显然的情况是,若 \(x,y_1,y_2,\cdots,x\) 满足 \(y_i > x\) ,那么 \(x \bmod y_i = x\) ,最后依然会出现 \(x \bmod x\)

但凡存在一个 \(1\) ,当且仅当 \(1\) 是第一位,否则总会导致 \(0\) 在中途出现,于是这个 \(0\) 会一直存在下去。

你妈的,我真的要靠自己硬想吗?或者去打表找结论?我不啊。练题的时候我不想打表。

对不起!我不打表!我要看一眼题解。我找不到核心 trick !可能我甚至也没学过核心 trick !太奇葩了这题。

题解:

本来想看一眼题解,然后思考的。然后发现看一眼不够,看两眼也不够……就直接看题解了。

不妨先让数组有序再分析。
有两种情况:

  • \(a_1 \neq a_2\) 。最后结果就是 \(a_1 \neq 0\) 符合条件。这个东西是需要观察到的,但是我自己想的时候没观察到。
  • \(a_1 = a_2\)
    • 若存在 \(a_x\) 满足 \(a_x \not \equiv 0 (\bmod a_1)\) ,则直接把 \(a_x\) 提到首位,即 \(a_x, a_1,a_2, \cdots, a_{x-1},a_{x+1},\cdots,a_{n}\) 。由于 \(0 < y=a_x \bmod a_{1} < a_1\) ,最后结果是 \(y \neq 0\) 符合条件。
    • 否则,看起来是无解。怎么证明?如果不存在这么一个,则说明 \(a_1=a_2 | a_k, k=3,\cdots,n\) 。感觉随便归纳一下就是了。\(a_1 \bmod a_k\) 只会让 \(a_k\) 消失,最后只剩下 \(a_1,a_2\) 也没啥办法。而\(a_k \bmod a_1\) 直接导致有 \(0\) 的出现,直接就毁了。
view
cin>>n;
L(i,1,n)cin>>a[i];
sort(a+1,a+n+1);
if(a[1]!=a[2])cout<<"YES\n";
else{
	int ok=0;
	L(i,2,n)ok|=a[i]%a[1]!=0;
	cout<<(ok?"YES\n":"NO\n");
}

总结:
总结就是背板啊。反正我能证明。只要证明了就能背板。证明不了就用经典结论、归纳、调整各种伪证,再背结论和背板。伪证都搞不了那就放弃呗。

12

https://codeforces.com/problemset/problem/1922/B

问:
\(n\) 根木棍,第 \(i\) 根长度是 \(2^{a_i}\) 。询问从其中选择三根木棍的方案数,使得他们能组成不退化的三角形。顺序不同算一种方案。

\(1 \leq n \leq 3 \cdot 10^{5}\)\(0 \leq a_i \leq n\)

思考:
这真算是构造题?我是不是写错了。无所谓也能写。
\(n < 3\) 直接滚出去,无解。
出于经验主义,我们肯定在这里要用类似哈希表的东西。并且要通过枚举划分集合,使得集合方便组合计数。

哎不是,这题我好像比较典?枚举什么呢?我们枚举最长的那根木棍,然后再算一下两个木棍能构成的值域前缀方案数。
然后发现木棍长度是 \(2^{a_i}\) ,值域前缀太大了,应该是故意把这个方法卡了。

那咋做呢。我们需要一些观察,假设 \(x \geq y \geq z\)
固定了长边是 \(2^{x}\)

  • 次长边若是 \(y=x\) ,最短边可以是 \(z=0,1,\cdots,x\)
  • 次长边若是 \(y=x-1\) ,那么最短边即使取 \(z=y=x-1\) ,他们也是退化的。
  • 次长边若是 \(y<x-1\) ,显然比 \(y=x-1\) 的情况更坏。

那么我们枚举最长边是第 \(i\) 个,长度为 \(2^{a_i}\)
次长边只能选择长度也为 \(2^{a_i}\) 的木棍,且要减去最长的那根木棍的贡献 \(1\)
最短边只能选择长度 \(\leq 2^{a_i}\) 的木棍,且要减去最长和次长的木棍的贡献 \(2\)
某种长度的木棍的历史出现次数好解决。对幂次开哈希表维护历史出现次数 \(c(a_i)++\) 就行。
小于等于某种程度的木棍的历史出现次数怎么解决?总不能想办法用什么数据结构求个前缀吧。只需要我们对 \(a\) 数组排序,然后直接统计历史木棍出现次数就解决了,实际上枚举到 \(i\) 的时候,历史上就出现了 \(i\) 根。

hack1:
过不了样例。
观察一下,发现:
上面的组合计数是合法的,当且仅当三根木棍的集合不交,也就是说当且仅当三根木棍不一样。
这个之前居然没注意。这个得注意的啊。这个错误非常关键,要背下来。我们不能一开始就默认三根木棍的集合不交,而是需要具体情况具体讨论。

修正1:
还是继续之前的分析。最长和次长的木棍必须是相等的。
我们现在直接预先对幂次处理出现次数。然后从小到大枚举幂次 \(i\) 。pre 维护 \(c_{1,2,\cdots,i-1}\) 的和。

  • 一种情况是,最长的两根木棍一一样,最短的那根严格更短。那么若 \(c_i \geq 2\) 则答案加上 \(\binom{c_i}{2} \times pre\)
  • 一种情况下,三根木棍长度都一样。那么若 \(c_i \geq 3\) 则答案加上 \(\binom{c_i}{3}\)
  • 然后让 \(pre\) 加上 \(c_i\)

hack2:
我们枚举值域的时候居然枚举了 \([1,n]\) ,需要注意木棍的幂次范围是 \([0,n]\)

反倒是清空的时候可以枚举 \([0,n]\) 清空 \(c(i)\) ,也可以枚举 \([1,n]\) 清空 \(c(a_i)\) ,这时候倒是等效的。直接枚举 \([0,n]\) 清空值域其实稍微科学一些。

view
int n;cin>>n;
L(i,1,n)cin>>a[i],c[a[i]]++;
sum=0;
pre=0;
L(i,0,n){
	if(c[i]>=2)sum+=1LL*c[i]*(c[i]-1)/2*pre;
	if(c[i]>=3)sum+=1LL*c[i]*(c[i]-1)*(c[i]-2)/6;
	pre+=c[i];
}
cout<<sum<<"\n";
L(i,0,n)c[i]=0;

总结:
就是一点点简单的观察啊。观察到次长的木棍只能和最长的木棍一样。
更深刻的,我们应该是利用了幂次的性质 \(2^{x-1}=2^{x}/2\) 。稍微用了点可控的分类讨论技巧。

  • 按照幂次的情况讨论,这很可控。
  • 幂次越小则会越不可控,所以我们逐渐缩小幂次讨论。然后直接就发现幂次一旦缩小,问题就没办法做了。

然后就是比较基础的组合计数,虽然我一开始居然计数错了……没事反正改正过来了,会错的点也背了(不能先入为主地认为开始选的若干个元素一定都会从不同的集合选出来),下次不会再出现这种错误了。

13

https://codeforces.com/problemset/problem/1916/C

问:
给一个大小为 \(n\) 的多重集 \(a_1, a_2, \cdots, a_n\)\(M\)\(O\) 轮流操作,\(M\) 先操作。每次轮到一个人操作的时候:

  • \(|a|=1\) ,游戏结束。
  • 否则选择不同的 \(i,j(1 \leq i,j \leq |a|)\) ,删除 \(a_i,a_j\) ,插入 \(\lfloor \frac{a_i+a_j}{2} \rfloor \cdot 2\)

\(M\) 希望让最后一个数尽可能大,\(O\) 希望让最后一个数尽可能小。俩人都足够聪明。对于 \(k=1,2,\cdots,n\) ,回答如果只有 \(a_1, a_2, \cdots, a_k\) 是一开始的游戏多重集,则最后剩下的那个数是啥。

思考:
\(k=1,2\) 的时候结果是非常显然的。
\(k>2\) 时。\(M\) 希望最后剩下的数尽可能大,那他的做法就是把当前最小的两个数扬掉。\(O\) 希望最后的数尽可能小,那他的做法就是把当前最大的两个数扬掉。证明再说。
\(k>2\) 时总共有 \(k-1\) 次操作。\(k=3\) 时两次操作是显然的,结果也是显然的。
\(k>3\) 时怎么做呢?我好像不会啊。

接下来,直接掏出我的看题解大法。

题解:
首先说明,上面的思考,猜测的结论是错的。即所谓一个人试图把当前最小的两个数扬掉。另一个人试图把当前最大的两个数扬掉,他们可能有些时候是对的,但在这里肯定是错的。

我们考虑规模相对大的情况,比如我们先把小数据特判,能避免一些边界和数据约束问题。

我们要注意,如果两个数被选择,如果奇偶性相同,那么 \(\lfloor \frac{a_i+a_j}{2} \rfloor\) 下取整是不会亏的,否则会亏。

更深刻的,我们注意到,\(\lfloor \frac{a_i + a_j}{2} \rfloor \cdot 2\) 替换掉 \(a_i, a_j\) ,当 \(a_i \equiv a_j (\bmod 2)\) ,对 \(sum\) 的贡献是 \(0\) ,当 \(a_i \not \equiv a_j (\bmod 2)\) ,对 \(sum\) 的贡献是 \(-1\)\(sum\) 在初始时是 \(\sum a_i\)

特别的,我们总能发现,当 \(|a|=1\)\(a_i=sum\) ,所以我们只需要关注 \(sum\) 的变化规律。

另外,我们注意,无论怎么操作,在这个操作结束后,总会让序列中增添一个偶数,所以序列中总存在一个偶数。

还有一个注意点,每有一个操作被执行,则有一个数减少。

由于偶数的存在性不可改变,\(M\) 的操作一定是尽可能把奇数扬掉,使得 \(O\) 能利用的奇数尽可能少。所以若存在两个奇数,\(M\) 总会选择这两个。

另一方面,考虑 \(O\) 的操作。若 \(O\) 选择两个奇数则直接亏,后面可选的奇数变少。若 \(O\) 选择两个偶数,看似不亏,实则多给了 \(M\) 一次删除奇数的机会。于是 \(O\) 最好的做法是直接选择一个奇数一个偶数,对 \(sum\) 造成 \(-1\) 的贡献。

目前操作的分析已经结束了。

接下来是对操作分析造成的影响进行分析。我们计数当前集合的奇数的个数为 \(cnt\)

  • \(cnt \equiv 0 (\bmod 3)\) 。则每一轮(俩人各操作一次),\(sum\) 的贡献会净减少 \(1\) 。于是元素和会在 \(sum - \frac{cnt}{3}\) 恒定下来,再往后 \(a_1\) 就会变成这个数。
  • \(cnt \equiv 1 (\bmod 3)\) 。则经过 \(\lfloor \frac{cnt}{3} \rfloor\) 轮后,元素和会达到 \(sum - \lfloor \frac{cnt}{3} \rfloor\) ,此时还剩下一个奇数。
    • 若此时 \(|a|=1\) 则答案已经明显是 \(sum - \lfloor \frac{cnt}{3} \rfloor\) 。此时原序列大小为 \(\lfloor \frac{cnt}{3} \rfloor + 1\)
    • 若此时 \(|a|=2\) ,只剩下唯一一种操作,即选择一个奇数一个偶数。在新的一轮 \(M\) 只能选择仅有的这种操作,使得答案变成 \(sum - \lfloor \frac{cnt}{3} \rfloor - 1\) 。此时原序列大小为 \(\lfloor \frac{cnt}{3} \rfloor + 2\)
    • 若此时 \(|a| \geq 3\) ,偶数是 \(\geq 2\) 的。在新的一轮 \(M\) 只能继续选择两个偶数,而 \(O\) 还有一次操作机会选择一个奇数一个偶数,于是最终元素和会在 \(sum - \lfloor \frac{cnt}{3} \rfloor - 1\) 恒定。此时原序列大小 \(\geq \lfloor \frac{cnt}{3} \rfloor + 3\)
  • \(cnt \equiv 2 (\bmod 3)\) 。则经过 \(\lfloor \frac{cnt}{3} \rfloor\) 轮后,元素和会达到 \(sum - \lfloor \frac{cnt}{3} \rfloor\) ,此时还剩下一个奇数。在新的一轮 \(M\) 会直接把这两个奇数扬掉,最终元素和会在 \(sum - \lfloor \frac{cnt}{3} \rfloor\) 恒定。

于是我们维护一个 \(f(i)\) 代表前 \(i\) 个数的奇数个数即可 \(O(1)\) 计算前 \(i\) 个数的多冲击的最终答案。

稍微整理一下 \(m\) 个数,开始的和是 \(sum\),一共有 \(cnt\) 个奇数的情况。

  • \(cnt \bmod 3 = 1\) ,并且 \(m \geq 2+\lfloor \frac{n}{3} \rfloor\) 则答案是 \(sum - \lfloor \frac{n}{3} \rfloor - 1\)
  • 否则,答案是 \(sum - \lfloor \frac{n}{3} \rfloor\)
view
cin>>n;
sum=0;
cnt=0;
L(i,1,n){
	int x;cin>>x;
	sum+=x;
	cnt+=(x&1);
	int r=cnt%3;
	if(r==1&&i>=2+cnt/3)cout<<sum-cnt/3-1<<" \n"[i==n];
	else cout<<sum-cnt/3<<" \n"[i==n];
}

14

https://codeforces.com/problemset/problem/1909/B

问:
给一个由两两不同正整数组成的数组 \(a_1, a_2, \cdots, a_n\) 。你需要严格执行一次以下操作:

  • 选择一个正整数 \(k\) 。对 \(i=1,2,\cdots,n\) ,将 \(a_i\) 改成 \(a_i \bmod k\)

找一个 \(k\) 满足 \(1 \leq k \leq 10^{18}\) ,使得经过操作后的 \(a_1, a_2, \cdots, a_n\) 严格有两个不同的数。

可以证明在数据范围内一定存在这么一个 \(k\)

\(2 \leq n \leq 100\)\(1 \leq a_i \leq 10^{17}\)

思考:
那么我们可以慢慢归纳。
假设 \(a\) 中存在奇数和偶数,\(k=2\) 就符合条件了。
否则全是奇数或者偶数,那么考虑 \(k=4\)
比方说全是奇数,他们模 \(2\) 全是 \(1\) ,那么模 \(4\) 只可能是 \(1,1+2=3\)
如果模 \(4\) 全是一样,比如说都是 \(x\) 。那么他们模 \(8\) 只可能是 \(x,x+4\)
以此类推。
如果一开始全是偶数,情况其实也差不多。

所以我们枚举 \(k=2^{1},2^{1},2^{2},\cdots\) 然后暴力检查即可。

其实感受上我们可以找到一个 \(k=2^{y}\) 满足 \(k \leq max a_i\) 。数据范围是对的。严谨证明再说。

这样我们其实通过 \(O(n \times \log 10^{18})\) 的时间就能找到 \(k\) 。其实只要对 \(\bmod 2\) 的分类有理解,然后往后对 \(\bmod 2^{y}\) 再稍微归纳一下就好了。

然后写一发,根据这个题的位置和我做题的经验,再加上我积累的一些小直觉,应该是对的。

hack:
好吧,实际上我错了。并且不是没找到 \(k\) 的那种错,因为特判程序的 RE 没有触发。
然后突然发现,\(a\) 数组输入达到了 \(i64\) 级别,这个我没开大。然后还是错。
我偷懒直接用了 set 当集合,而我 set 也只是 int,这个没开大。改了就对了。

一般输入都是 i32 对吧,突然来个 i64 就把你 gank 了!那你各种容器都得调成 i64 啊,而不是忘记调了,或者只去调输入数组。

下面是实现,就是从 \(2\)\(1\) 次往后幂暴力枚举它的幂次,然后暴力 \(O(n)\) 检查。

view
cin>>n;
L(i,1,n)cin>>a[i];
L(z,1,60){
	i64 k=1LL<<z;
	set<i64> S;
	L(i,1,n){
		S.insert(a[i]%k);
	}
	if(SZ(S)==2){
		cout<<k<<"\n";
		break;
	}
}

时间是 \(O(n w)\) 的,如果偷懒用 set 的话再带个 \(\log n\)

补充证明:
说实话这个是我临时观察到的,现在给出严谨证明,方便以后直接用。

证明先鸽一下,我把题往后写一写。

15

https://codeforces.com/problemset/problem/1907/C

问:
给一个长度为 \(n\) 的小写拉丁字符串 \(s\) 。每次操作可以选择字符串中相邻两个位置,满足这两个位置的字符不一样,然后删除他们。询问最终能使得 \(s\) 达到的最短长度。

思考:
稍微分析一下发现不会做。

然后被提示了一下,说是摩尔投票推论。

那么我声称我可以直接得出结论:

然后证明也容易:摩尔投票推论+鸽笼原理推论。
\(c_1, c_2,\cdots,\) 是每种颜色的数量,且不妨按降序排序。设 \(sum\) 是所有颜色的总和。

  • \(c_1 \leq \frac{sum}{2}\) ,由摩尔投票推论总能尽可能两两配对(全配完或者余下 \(1\) 个)。
    • 如果不要求相邻,一个合法的配对方式是,让当前不同色配对的两个颜色之一,有一个颜色是当前剩余最多的颜色。
    • 如果要求相邻,那么存在当前剩余最多的颜色的一个位置,满足一个相邻位置是另一种颜色。我们的算法是找到整个位置,消去两个相邻不同色。
    • 否则若找不到这种位置,由鸽笼原理整个序列只有一个颜色。如果按照上述算法得到了只剩下一种颜色的情况,则又和摩尔投票是矛盾的。
    • 于是最终能剩下的字符数量就是 \(n \bmod 2\)
  • \(c_1 > \frac{sum}{2}\),由摩尔投票,能够配对的最多对数是 \(sum - c_1\)
    • 按照上述的算法,我们能够完成这些对数的配对。并且最终会剩下 \(c_1 - (sum - c_1) = 2 \cdot c_1 - sum\)\(1\) 号(开始时最多的)颜色。
    • 那么剩下的字符数量就是 \(2 \cdot c_1 - n\)

实现也简单。如下。

稍微值得注意的是 \(p_0, p_1, \cdots, p_{25}\) 对应的是颜色,别记成对应的是 \(1 \sim n\) 。这里我写的时候出了一次 bug 。

view
cin>>n;
cin>>s;
L(i,0,25){
	c[i]=0;
	p[i]=i;
}
L(i,0,n-1){
	c[s[i]-'a']++;
}
sort(p,p+26,[&](int i,int j){
	return c[i]>c[j];
});
if(c[p[0]]<=n/2)cout<<n%2<<"\n";
else cout<<2*c[p[0]]-n<<"\n";

总结:
好像没有需要我证的啊。就是摩尔投票+鸽笼原理。
我中间猜了一个大概率对的结论:摩尔投票的配对方式可以是让当前配的不同色对总有一个是当前出现次数最多的颜色。感觉显然是对的。
题目其实比较有趣,尤其是这种相邻位置配对和任意位置配对之间的联系,我是临时观察然后猜也是临时证的。背一背题其实挺好的。

16

https://codeforces.com/problemset/problem/1903/B

问:
给一个 \(n \times n\) 的非负整数矩阵 \(M\) ,你需要构造一个数组 \(a_1, a_2, \cdots, a_n\) 满足:

  • \(0 \leq a_i < 2^{30}\)
  • \(\forall i \neq j, M_{i,j} = a_i | a_j\)

如果不能构造,回答不能。否则回答任意一种构造。

\(1 \leq n \leq 10^{3}\)\(0 \leq M_{i,j} < 2^{30}\)

思考:
首先注意 \(a_i\) 对自己是没有贡献的,这使得构造相对宽松,否则答案是最多只可能是主对角线上的数。

感觉这个构造对一些有经验的选手来说应该是非常显然的。

首先其实每个二进制位都是独立贡献的,所以我们可以直接拆位去做,对于每一位就是一个 \(0,1\) 矩阵。
\(M_{i,j}=0\) ,那么 \(a_i,a_j\) 一定都是 \(0\) ,这就能直接构造。
\(M_{i,j}=1\) ,那么 \(a_i,a_j\) 存在一个 \(1\) ,直接构造不可控,很容易产生后效性。如果先把所有 \(M_{i,j}=0\) 的情况考虑完了,再考虑 \(M_{i,j}=1\) 的情况,往后构造就是可控的。

  • \(a_i,a_j\) 都填入了,则检查是否满足存在有一个 \(1\) ,不能则无解。
  • \(a_i,a_j\) 有一个填入了。若是 \(0\) 则只能给另一个填入 \(1\) 。若是 \(1\) 另一个其实也不妨填入 \(1\) ,因为目前只剩下 \(M_{i,j}=1\) 的情况, \(0\) 对这种情况没有贡献。总结来说就是这种情况最优解总是把那个未填入的数填成 \(1\)
  • \(a_i,a_j\) 两个都没填入,则不妨填入两个 \(1\)

于是构造方案就有了,如果中间没有矛盾,那么每一个二进制位下的 \(a\) 都可以构造,最后合并起来就是答案。

感觉难写吗。拆二进制,然后合并,应该是很板的。
矩阵可以遍历两边,先处理 \(M_{i,j}=0\) 的情况,再处理 \(M_{i,j}=1\) 的情况,分类讨论也是不多的。

然后 \(O(n^{2} w)\) \(TLE\) 了?\(10^{6} \times 30\) 怎么 \(TLE\) 呢。对不起数组开小了……改了一下就对了。

下面是代码。

view
cin>>n;
L(i,1,n)L(j,1,n){
	cin>>g[i][j];
}
ok=1;
L(k,0,30){
	L(i,1,n)b[i]=-1;
	L(i,1,n){
		L(j,1,n)if(i!=j){
			int w=g[i][j] & (1 << k);
			if(__builtin_popcount(w)==0){
				b[i]=0;
				b[j]=0;
			}
		}
	}
	L(i,1,n){
		L(j,1,n)if(i!=j){
			int w=g[i][j] & (1 << k);
			if(__builtin_popcount(w)==1){
				if(b[i]==-1&&b[j]==-1){
					b[i]=1;
					b[j]=1;
				}else if(b[i]==0&&b[j]==0){
					ok=0;
				}else{
					if(b[i]==-1)b[i]=1;
					if(b[j]==-1)b[j]=1;
				}
			}
		}
	}
	L(i,1,n)if(b[i]!=-1)a[i]+=b[i]<<k;
}
if(!ok)cout<<"NO\n";
else{
	cout<<"YES\n";
	L(i,1,n)cout<<a[i]<<" \n"[i==n];
}
L(i,1,n)a[i]=0;

然后是一些实现要注意的地方。

  • 枚举 \(1 \leq i,j \leq n\) 的时候,我们只枚举 \(i \neq j\) 的情况,\(i\) 对自己这个位置是没贡献的。
  • 我们每次单独初始化 \(b\) 数组是 \(-1\) 以区分他在当前二进制位下,这一位选没选,选的是什么。
  • \(b\) 合并到 \(a\) 中的时候,会存在一些没选的位置。比如 \(n=1\) 的时候。这些位置无论是 \(0\) 还是 \(1\) 都不会产生贡献,但总要选一个合并到 \(a\) 中。

总结:
观察到可以拆位处理。在拆位后就变成了 \(0,1\) 情况的处理。然后按照可控优先的情况先用 \(0\) 构造再用 \(1\) 构造,实际做题的时候要一些枚举和讨论。

其他实现:

然后我感觉我代码很长,有没有更优雅一些的实现?

然后找了下其他人的代码,很多人都没有拆位去做,而是直接做了。

这是一种新方法。

首先让所有的 \(a_i = 2^{30}-1\) 。这有什么好处呢?不清楚,再看。
然后选择 \(\forall i \neq j\) 执行 \(a_i \&=\ M_{i,j}\)\(a_j \&=\ M_{i,j}\)

这个证明其实不妨拆位去证。对于每个二进制位相当于初始化为 \(1\) ,然后 \(AND\) 操作可以保证:

  • \(M_{i,j}=0\) ,则 \(a_i,a_j=0\) 。这是唯一可行的操作。
  • 否则若 \(M_{i,j}=1\) ,则直接不妨让 \(a_i,a_j=1\) 。可行的操作是让 \(a_i,a_j\) 存在 \(1\) ,如果让 \(a_i,a_j=1\) ,则后续不影响 \(a_i,a_j\) 调整到 \(0\)
    • 如果后续某个操作结束后,\(a_i=0 \wedge a_j=0\) ,说明后续那个操作后当前的操作矛盾,即无法构造。

于是我们一遍就可以构造出来。

然后再遍历一遍查询构造出来的数组是否合法。

view
cin>>n;
L(i,1,n)a[i]=(1<<30)-1;
L(i,1,n)L(j,1,n){
	cin>>g[i][j];
	if(i==j)continue; // 选择 i==j 会导致构造没法做 
	a[i]&=g[i][j];
	a[j]&=g[i][j];
}
ok=1;
L(i,1,n)L(j,1,n)if(i!=j){
	ok&=(a[i]|a[j])==g[i][j]; // 注意二元逻辑运算符的优先级比 == 还低 
}
if(!ok)cout<<"NO\n";
else{
	cout<<"YES\n";
	L(i,1,n)cout<<a[i]<<" \n"[i==n];
}

hack:

  • 一开始选择了 \(\forall i,j\) 没考虑跳过 \(i=j\) 的情况,这会导致构造没法做。实际上还是对这个做法理解不够深刻。
  • 我居然写了 \(x|y==z\) 这种,不知道与、或、异或这种二元逻辑运算符比 \(==\) 还慢的吗。

这两个 bug 或者说易错点在注释里写出来了。

总结:
这个方法实现起来很快,思路其实相当于我第一个做法的逆向思路。证明起来可以拆位去证明,但是实现的时候却并不需要拆位实现。

无论是代码实现速度,还是代码跑的速度,都比直接拆位讨论更快。

17

https://codeforces.com/problemset/problem/1846/D

讲个鬼故事,这题能 1200 是不是以为外国人人均不会数学?

问:
给出等腰三角形长为 \(d\) 高为 \(h\) ,他们会和一棵圣诞树一样在一条数周上打印,给 \(n\) 个三角形的底边高度 \(y_i\) 。如果这些三角形刻意看成圣诞树,询问圣诞树的面积。

思考:

  • 梯形面积:上底边加下底边乘以高除以二。\(\frac{1}{2} \cdot (d_{down} + d_{up}) \cdot l\)
  • 相似三角形:\(d_{down} / d_{up} = h / (h - l)\) 。其中 \(l\) 是梯形高度。根据相似三角形公式可以计算上底边化简有 \(d_{up} = d_{down} * (h - l) / h\)

不妨假设 \(y_1, y_2, \cdots, y_n\) 是从小到大有序的。否则不妨排序。

对于 \(i=1,2,\cdots,n-1\) ,我们能计算出 \(l=min(h,y_{i+1}-y_{i})\) 。对于 \(i=n\) ,我们不妨设 \(y_{n+1}\) 解决边界问题。
\(i\) 个三角形对答案的贡献是 \((d_{down} + d_{up}) \cdot l \frac{1}{2}\)

代码真的就只是推一推公式,看清楚变量是啥,然后就能写。

view
cin>>n>>d>>h;
L(i,1,n)cin>>y[i];
sort(y+1,y+n+1);
y[n+1]=INF;
ans=0;
L(i,1,n){
	int h_=min(y[i+1]-y[i],h);
	// d/d_ = h/(h-h_) -> d_ = d*(h-h_)/h
	long double d_=1.L*d*(h-h_)/h;
	ans+=1.L*(d_+d)*h_/2;
}
cout<<fixed<<setprecision(10)<<ans<<"\n"; 

阶段性总结:
这个题,什么梯形面积、相似三角形公式,都是中国人从小学过的。而我发现其他 1200 的题到最后基本都是板。也就是中国人小时候学的板子和外国人小时候学的板子不一样对吗。多刷就能知道 trick 了。

2025/05/10/16.00

2025/05/22/03.03

18

https://codeforces.com/problemset/problem/1846/C

问:
\(n\) 个人,\(m\) 个题,比赛持续 \(h\) 分钟。会给 \(n\) 行,每行 \(m\) 个数,第 \(i\) 行第 \(j\) 个数代表第 \(i\) 个人写第 \(j\) 道题需要花的时间。

罚时累计规则为写完一道题目时所经历的分钟。比赛结束时,不同题数高题数排名高,相同题数低罚时排名高。

\(1\) 个人是你。如果每个人都按最优策略开题,询问你的最终排名。如果最终你与其他人并列排名,则认为你排名更高。

思考:
每个人都按最优策略,那每个人都可以单独考虑。按照惯例应该是考虑邻项交换,比如考虑两个相邻数,怎么排能让前缀和贡献更小就怎么排,稍微感受一下最后化简出来本质上相当于直接按完成题目所需的时间排序。

然后处理出前缀和,线性找出每个人在规定时间内能完成的题量和罚时,然后用排列置换数组加排序下就好了。

如果不存在并列情况,双关键字排序就好了,但是现在要对并列区分,实际上就是复杂点的偏序,三关键字排序就能搞定。

感受一下应该很对,写一发。

view
int n,m,h,sum,cnt;
i64 dirty;
int p[MAXN],w[MAXM];
array<i64,3> a[MAXN];

signed main(){
	cin.tie(nullptr)->ios::sync_with_stdio(false);
	
	int _;_=1;
	for(cin>>_;_;_--){
		cin>>n>>m>>h;
		L(i,1,n){
			L(j,1,m){
				cin>>w[j];
			}
			sort(w+1,w+1+m);
			cnt=0;
			dirty=0;
			sum=0;
			L(j,1,m){
				if(sum+w[j]>h)break;
				sum+=w[j];
				dirty+=sum;
				cnt++;
			}
			a[i]={cnt,dirty,i};
			p[i]=i;
		}
		sort(p+1,p+1+n,[&](int i,int j){
			if(a[i][0]!=a[j][0]) return a[i][0]>a[j][0];
			else if(a[i][1]!=a[j][1]) return a[i][1]<a[j][1];
			else return a[i][2]<a[j][2];
		});
		L(i,1,n)if(p[i]==1){
			cout<<i<<"\n";
			break;
		}
	}
	return 0;
}

唐的地方:

  • 果不其然 \(j\)\(i\) 是冲突了的,但是这种 bug 打印调试很好调。
  • 果不其然有个细节是如果 \(sum\) 加上 \(w\) 越界了那么不能加而应直接中断,写成了如果现在的 \(sum\) 加上 \(w\) 越界了再做中断,确实是以前没养成好的逻辑。现在应该注意一下,并且还好这种毛病打印调试也好调。
  • 排名这种事情,肯定是越小越好,而不是越大越好,一开始还没分析出来,打印排名后观察了下才发现。这也是有点逻辑问题,并不是什么都越大越好。
  • 之前没深究排列数组 \(p\) ,排序后的 \(p(i)\) 应该是第 \(i\) 个位置原来编号是 \(p(i)\) ,验证一下显然是很有道理。而非是原来第 \(i\) 个位置排完后位置是 \(p(i)\)

19

https://codeforces.com/problemset/problem/1822/D

问:
给定一个排列 \(a\) ,我们构造 \(b\) ,有 \(( b_i = \sum_{j=1}^{i} a_j ) \bmod n\) 。如果 \(a\) 是超级排列当且仅当 \([b_1 + 1, b_2 + 1, \cdots, b_n + 1]\) 也是个排列。

给一个 \(n(1 \leq 2 \cdot 10^{5})\) ,询问能否构造大小为 \(n\) 的超级排列 \(a\) ,如果能,输出任意一个构造的 \(a\)

思考:
很神奇的构造题,感觉只能手玩、归纳、找规律。等一下,这题我好像是做过的。

说结论。

我们考虑 \(a_k=n\) 的位置 \(k\) 。当 \(k>1\) ,根据递推式有 \(b_k = b_{k-1} + a_{k} = b_{k-1}\) ,这在显然矛盾。

为什么要这样想呢,傻孩子,一是从递推式去想是很显然的,二是当递推式加上 \(n\) 的时候没有贡献也是显然的。

\(n=1\) 时答案是显然的,所以只考虑 \(n>1,k=1\) 。只有这种情况可能是对的,但不一定这个约束下就足够正确,可能约束要更严格。

在这个约束下,\(b_n = ( \sum_{j=1}^{j=n} a_j ) \bmod n = \frac{n(n+1)}{2} \bmod n\) ,如果 \(n \mid b_n\) ,又会和 \(n \mid b_1\) 矛盾,显然 \(n>1\) 且是奇数时是矛盾的。

现在一个可能的约束是 \(n>1\) 且是偶数,且 \(a_1=n\) 的约束下,可能存在构造答案。

事实上是可以构造的。

  • \(k=1\)
  • \(n>1\) 时,\(n\) 只能是偶数。

接下来就是上板子了。第一位开始,让偶数递减,从第二位开始,让奇数递增,这样交替填入。形式化地说,如果第 \(i\) 个位置是奇数位则填入 \(n-i+1\) ,如果第 \(i\) 个位置是偶数位则填入 \(i-1\) 。比如前几个 \(a\)\(n,1,n-2,3,n-4,5,n-6,7,\cdots\) ,对应的 \(b\)\(0,1,n-1,2,n-2,3,n-3,4,\cdots\)
证明就是,数学归纳大法好。
归纳假设 \(b_{2(l)}=l \bmod n\) ,则 \(b_{2l+2} = (b_{2l} + a_{2l+1} + a_{2l+2}) \bmod n = ( l + (n-(2l+1)+1) + (2l+2-1) ) \bmod n = (l + n + 1) \bmod n = l+1\) 。得证。
归纳假设 \(b_{2l+1}=(n-l) \bmod n\) ,则 \(b_{2(l+1)+1}=b_{2l+1}+a_{2l+2}+a_{2l+3}=(n-l)+(2l+2-1)+(n-(2l+3)+1)=(2n-l-1) \bmod n = (n-l-1) \bmod n = (n-(l+1))\) 。得证。
偶数位置取满 \(1 \sim \frac{n}{2}\) ,奇数位置取满 \(\frac{n}{2} + 1 \sim n\) 。模 \(n\) 意义下再加 \(1\) 构成一个排列。

总结:

  • 从递推贡献角度考虑看起来是个很优秀很常见的思考方法。这里能考虑到,当 \(n\) 被一个项加上后,必然没有贡献,从而导致下一个项依旧一样。
  • 对排列拆分奇偶,考虑按不同的偏序和顺序交替填入,看起来是排列构造里关于前缀和的一个挺常见的板。

下面是代码,希望能一发对。

view
cin>>n;
if(n==1)cout<<1<<"\n";
else if(n&1)cout<<-1<<"\n";
else{
	r=n;l=1;
	L(i,1,n){
		cout<<(i&1?n-i+1:i-1)<<" \n"[i==n];
	}
}

结果依旧没有一次对,因为用了 \(r--,l++\) ,应该是 \(r+=2,l-=2\) 才对。
事实上我又马上发现可以直接分奇偶填入我预期的 \(a_i\) ,上面是算过的,奇数填入 \(n-i+1\) ,偶数填入 \(i-1\)

20

https://codeforces.com/problemset/problem/1799/B

问:
给一个正整数数组 \(a_1, a_2, a_3, \cdots, a_n\) 。允许执行以下操作任意次:

  • 选择 \(i,j(1 \leq i,j \leq n,i \neq j)\)
  • \(a_i\) 变成 \(\lceil \frac{a_i}{a_j} \rceil\)

如果经过一些操作后,\(a\) 数组所有元素一样,可以证明最多需要 \(30n\) 次操作。

回答是否可以做到,如果能,则在 \(30n\) 次操作内完成,并输出操作。

思考:
就是益智游戏呗。我们知道核心点是除法是指数级的,所以很容易构造。

如果一开始就一样,那需要 \(0\) 次操作。

注意什么情况下无解,一种无解情况是 \(1,2\)\(2\) 除以 \(1\) 并不会有贡献。而 \(1\) 除以 \(2\) 变成了 \(0\) 那就更坏了,因为显然没办法最终都变成 \(0\) ,证明感觉反证会很容易。

那么如果存在 \(1\) ,只可能最后都变成 \(1\) ,但是好像也显然是不行的,最后总有一个数变不成 \(1\) ,应该也容易证明。
hack: 如果一开始都是 \(1\) ,那么显然是合理的。而存在 \(1\) 但不全是 \(1\) 才是上面说的那种情况。但好在我们可以一开始判断掉全是相等的情况,后面让大前提是一开始并非全都相等。

观察以下样例发现应该挺对。那么存在 \(1\) 的情况被考虑完了。接下来只需要考虑存在 \(> 1\) 的数。

如果存在 \(2\) ,最终肯定可以通过除以 \(2\) 上取整变成全是 \(2\) ,看 \(10^{9}\) 的数据范围,在 \(30n\) 次内是完全够用的。

否则,我们是否可以通过一些操作构造出 \(2\) 。关键在计算操作数会不会超。如果得到了 \(2\) ,接下来还剩 \(n-1\) 个数每个数要用 \(\lceil \log_{2} 10^{9} \rceil = 29\) 次。这意味着我们有 \(n-1 + 30\) 次的操作供我们构造 \(2\) ,看起来是完全足够的。

一个想法是随便选两个不同的数辗转相除上取整,复杂度是 \(\log_{2} 10^{9}\) 的,看起来可以在规定操作数内构造。但是发现存在一个问题,经过一些操作后可能使得这两个数一样,并且最后不是 \(2\)

想了一会儿发现没有上面很确定的做法,去看一眼题解。

看了一眼题解:

服了,发现 \(n\) 很小,只有 \(100\) 。之前有一个感觉会对但是又感觉不是 \(O(n)\) 的做法是:一直找最大的除以最小的。可以归纳证明是对的。然后复杂度确实也是 \(O(n^{2} \log m)\) 的。这样就对了。

归纳中一个本质的事情是:一个数除以另一个数,至少变成二分之一

由于上取整,除非中途所有数都一样了,否则存在两个数不一样,也就是我们总能找到一个最小最大值,如果这个程序一直持续下去,最终将出现一个 \(2\) 。所以最多经过 \(30n\) 的轮次就能让所有数一样。每轮是 \(O(n)\) 的。

下面是代码。

中间是 WA1 了,说样例没过,我看样例是过了啊?
我的天他说是按 \(i,j\) 输出,代表 \(a_i\) 变成 \(\lceil \frac{a_i}{a_j} \rceil\)
我们的算法是 \(a_i\) 是最大值 \(a_j\) 是最小值,而我把最小值放前面了……

下面是真代码。

view
cin>>n;
L(i,1,n){
	cin>>a[i];
}
if(count(a+1,a+1+n,a[1])==n){
	cout<<0<<"\n";
}else if(count(a+1,a+1+n,1)>0){
	cout<<-1<<"\n";
}else{
	ans.clear();
	while(true){
		int ok=1;
		L(i,1,n-1)ok&=a[i]==a[i+1];
		if(ok)break;
		int l=-1,r=-1,mi=INF,mx=-INF;
		L(i,1,n){
			if(mi>a[i])mi=a[i],l=i;
			if(mx<a[i])mx=a[i],r=i;
		}
		a[r]=(a[r]+a[l]-1)/a[l];
		ans.psb({r,l});
	}
	cout<<SZ(ans)<<"\n";
	for(auto info:ans){
		cout<<info[0]<<" "<<info[1]<<"\n";
	}
}

注意下顺序,先判断是否全部一样,再判断是否不全一样的情况下存在 \(1\) ,否则再做 \(O(n^{2} \log m)\) 的算法。

至此,\(900 \sim 1200\) 的构造题就完结了。中间写着还是很疑惑,写多了之后确实发现,都是比较有趣且经典的 puzzle 例子。

但是看了一下发现凑到 \(1300\) 应该差不多,再补 \(20t\) \(1300\) 的了。

1300 构造

1

https://codeforces.com/problemset/problem/2108/B

问:
给出 \(n,x\) ,构造一个正整数数组 \(a\) 满足 \(a_1 \oplus a_2 \oplus \cdots \oplus a_n = x\) 。你需要回答是否能构造,如果能,则构造 \(a\) 的元素和 \(\sum_{i=1}^{n} a_i\) 最小的一种,并输出这个和。

\(1 \leq n \leq 10^{9}, 0 \leq x \leq 10^{9}\)

思考:
就是说我们要在 \(\sum_{i=1}^{n} a_i\) 最小的情况下构造 \(a_1 \oplus a_2 \oplus \cdots a_n = x\)\(a_i > 0\) 。这咋搞啊。

第一次的思路:

因为我们要构造正整数数组 \(a\) ,而 \(x\) 可能是 \(0\) ,所以我们先考虑 \(x=0\)

如果 \(x=0\)

  • \(n=1\) ,显然只能 \(a_1=0\) ,构造不了正整数 \(a\)
  • \(n>1\) 且是偶数,则全构造成 \(a_i=1\) ,显然是最小且合法的。和是 \(n\)
  • \(n>1\) 且是奇数,不难验证构造 \(n-3\)\(1\) 和二进制的 \(11,01,10\)\(3,1,2\) 是最小且合法的。和是 \(n-3+1+2+3=n+3\)

接下来只需要考虑 \(x>0\)
\(n\) 是奇数,感性手玩一下大概是 \(n-1\)\(1\) 抵消,再放一个 \(x\) 是最小且合法的。

接下来只需要考虑 \(2 \mid n,x > 0\)
不妨构造 \(n-2\)\(1\) 让他们抵消,最后考虑 \(x\) 怎么处理。注意 \(\oplus\) 是二进制下不进位的 \(+\) ,我们随便拆分成 \(u+v=x\) 且不产生进位,那么 \(u,v\) 对答案的贡献是一样的。如果 \(x\) 是二的幂次则不能构造 \(u,v\) ,则不能构造 \(a\)

hack1:
\(n=2,x=1\) ,满足 \(2 \mid n,x > 0\) ,这里 \(x\) 是二的幂次,按之前的考虑应该是不难构造的。而且因为先入为主的思维定势我确实也没想出怎么构造。实际上存在构造方法是 \(2,3\) 即二进制下的 \(10,11\)

这意味着什么?这意味着,我们如果不能在不产生进位的情况下拆分二的幂次,那就在产生进位的情况下拆分二的幂次嘛。注意让这个进位的位尽可能低就行。

  • 实际上如果 \(x=2^{0}\) ,这个位置可以是第 \(1\) 位。
  • \(x=2^{y>0}\) ,这个位置可以是第 \(0\) 位。

看起来应该没对答案增加太多的贡献。

hack2:
样例 \(n=15,x=43\)\(a\) 的最小合法和应当是 \(55\) ,而算法给出了 \(57\) 。实际上我们是 \(14\)\(1\) 和一个 \(43\) ,确实搞出了 \(57\) 。看起来是之前的感性猜测是伪证。
确实存在一个更优的方案,比如我们构造 \(13\)\(1\) ,然后最后两个位去拆分 \(x\) ,那么就省下了 \(1\) 的贡献,可以得到 \(56\) 的结果。但显然不是最优。
继续观察发现依旧存在更优的方案,直接把 \(x\) 按二进制拆开。

  • 如果不能填满 \(n\) 个位置。
    • 如果剩下的位置是偶数个,全填上 \(1\) 。答案就是 \(x+n-z\)\(z\) 要尽可能大,实际上是 \(popcount(x)\) ,要满足 \(2 \mid n-z\)
    • 如果超过了 \(n\) 个位置,那么我们存在拆成 \(n\) 个数的方法。那么答案其实就是 \(x\)实际上答案也是 \(x+n-z\) (这里 hack 了,此时 \(z \geq n\) 应是确定的,且不能确定 \(z=n\) ,那么答案就是 \(x\) )。
    • 如果剩下的位置是奇数个,我们要少拆一个数,然后补满偶数个 \(1\) ,即多补一位的 \(1\) 。那么答案就是 \(x+n-z+1\)
      • 但是这里是拆的数规模相对大的情况下才能少拆,如果少拆一个变成拆了 \(0\) 个,那就搞不了。也就是说 \(n\) 是偶数, \(x\) 是二的幂次要单独考虑。

如果 \(x\) 是二的幂次,并且要拆出大于一个数。我们其实有办法亏贡献,多两个位置的进位把他拆成两个数。剩下的位置是 \(n-2\) 是偶数个,那么都填 \(1\) 就行。进位的位选哪个只要看 \(x\) 是否是 \(1\) 就行。

整理一下,我们按顺序处理。
先处理 \(x=0\) 的情况。再处理 \(2 \mid n,x=2^{y}\) 的情况。再否则我们可以直接按 \(x\) 的二进制拆。

优化了一下感觉不一定是最优,但是样例是给过了。代码一交果然给 \(WA\) 了。

接下来就去看一眼题解了。

看了一眼题解后的思考:
我感觉最终题解思路和我其实差不多啊。
核心点是我们要注意到异或是二进制下不进位的加法,在这个题里我们可以“除非必要,否则尽量构造不进位”。

如果 \(x=0\) ,只有一种构造方法,前面 \(n-3\) 位全填 \(1\) ,最后三位填 \(1,2,3\) ,答案就是 \(n+3\) 。题解说可以证明是最优的,现实中我们确实手玩不出更好的方法。
当然要求 \(n \geq 3\)\(n \geq 2\) 稍微手玩一下都知道搞不出正整数数组。

如果 \(x \geq 1\) 。让 \(c\)\(x\) 的二进制下 \(1\) 的数量。若 \(n \leq c\) 那么很好构造,随便按二进制拆一下就行。否则要考虑 \(n > c\) 的情况。

如果 \(n-c\) 是偶数,我们只要填入 \(n-c\)\(1\) 就行,题解说可以证明是最优的。否则就看 \(n-c\) 是奇数。

上面其实我最后想到的都和题解一样。而这里开始可能不一样了。

我是怎么做的呢。让 \(x\) 直接构造成 \(c-1\) 个位置,当然需要 \(c > 1\) ,然后再多补一个 \(1\) 。若 \(c=1\) ,那么看 \(x\) 是否是 \(2^{0}\) ,如果是 \(2^{0}\) ,前面 \(n-c-1\) 个位置填 \(1\) ,然后补一个 \(2^{1}\) ,一个 \(2^{1} + 2^{0} = 3\) ,即 \(n-c-1+2+3=n-c+4=n+3\) ;如果不是,则前面 \(n-c-1\) 个位置填 \(1\) ,然后补一个 \(1\) ,一个 \(x + 2^{0} = x+1\) ,即 \(n-c-1+1+x+1=n-c+x+1=n+x\)

这居然是错的吗?题解怎么搞的?题解是一样的!只不过分类讨论得更简洁。而我哪里错了呢?

我的天,我的原 WA 掉的代码

cin>>n>>x;
if(x==0){
	if(n==1)cout<<-1<<"\n";
	else if(n&1)cout<<n+3<<"\n";
	else cout<<n<<"\n";
}else if((~n&1)&&(__builtin_popcount(x)==1)){
	if(x==1)cout<<n-2+x+4<<"\n";
	else cout<<n-2+x+2<<"\n";
}else{
	int z=__builtin_popcount(x);
	if(z>=n||(n-z)%2==0)cout<<x<<"\n";
	else cout<<x+n-z+1<<"\n";
}

改过之后 AC 的代码

cin>>n>>x;
if(x==0){
	if(n==1)cout<<-1<<"\n";
	else if(n&1)cout<<n+3<<"\n";
	else cout<<n<<"\n";
}else if((~n&1)&&(__builtin_popcount(x)==1)){
	if(x==1)cout<<n-2+x+4<<"\n";
	else cout<<n-2+x+2<<"\n";
}else{
	int z=__builtin_popcount(x);
	if(z>=n)cout<<x<<"\n";
	else if((n-z)%2==0) cout<<x+n-z<<"\n";
	else cout<<x+n-z+1<<"\n";
}

可以看到我整理条件的时候,误操作把 \(c \geq n\)\(n > z \wedge 2 \mid (n-z)\) 整理成了一种答案 \(x\) ,实际上 \(c \geq n\) 答案是 \(x\) ,而 \(n > z \wedge 2 \mid(n-z)\) 答案是 \(n-c+x\) ,再否则是 \(n-c+x+1\)

总结:
好蠢啊,整理条件的时玩抽象,什么漏整理或者整理重了就很唐。这个实际上还是要大量刷题练上去的。

核心是知道异或是二进制下不进位的加法,然后能想到尽可能构造不进位的情况,最后也能发现 \(x=0\) 很特殊,而一定需要进位的 case 也非常少。

2

https://codeforces.com/problemset/problem/2101/A

问:
\(0,1,\sim,n^{2}-1\)\(n^{2}\) 张卡片,需要填入 \(n^{2}\) 的矩阵。需要让所有 \((\frac{n(n+1)}{2})^{2}\) 个子矩阵的 \(mex\) 和最小。给 \(n\) ,构造矩阵。

思考:
啊!又是这种题。首先为什么子矩阵共有 \((\frac{n(n+1)}{2})^{2}\) 个呢?这个还是挺有意思的,手玩一下发现确实对。
你要我去枚举所有子矩阵我肯定是 \(O(n^{4})\) 枚举啊。要么是枚举四条线 \(n \cdot n \cdot n \cdot n\) ,要么是枚举两个点 \(n^{2} \cdot n^{2}\)

更精确的计算是他实际上会有个 \((\frac{n(n+1)}{2})^{2}\) 子矩阵。首先确实这个东西另一个式子是 \(\sum_{i=1}^{n} i^{3}\) 。有什么想法没?没有。
也注意到 \(\sum_{i=1}^{n} i^{3}=(\sum_{i=1}^{n} i)^{2}\) 。显然这里我们可以两维分开计算。只考虑一维,共有 \(n\) 个位置,如果选择了左端点是 \(i\) ,右端点的位置只能 \(\geq i\) ,其实总一维下来两个端点的选择方案就是 \(\sum_{i=1}^{n} i = \comb{n+1}{2}\) ,两维乘起来显然是对的。这就是精确的子矩阵个数计算。

还是想想怎么做题吧。所有子矩阵的 \(mex\) 之和最大,你是真逆天啊。但是这个应该是容易分析的。

首先对于 \(1 \times 1\) 的子矩阵,只有一个子矩阵 \(mex=1\) ,其他 \(n^{2}-1\) 个子矩阵的 \(mex=0\)\(1 \times 1\) 的子矩阵对和的贡献是固定 \(=1\) 的。
那么我们考虑更大的规模,至少要让尽可能更多的子矩阵覆盖住 \(0\) 这个点。

稍微玩一下感觉是从 \(0,1,2,\cdots\) 按顺序考虑,要尽可能多的子矩阵覆盖这些数字。那么看起来是从中间开始,逐渐绕圈填就行。
但是绕圈的代码好不想写啊……我想想其实有个切比雪夫距离 \(max(x-x_0,y-y_0)\) 。如果 \(n\) 是奇数,可以找到中间点是 $(\lceil \frac{n}{2} \rceil, \lceil \frac{n}{2} \rceil) $ ,然后可以顺序枚举切比雪夫距离 \(0,1,2,\cdots,\lceil \frac{n}{2} \rceil\) 。问题是我不懂按切比雪夫距离枚举。然后如果 \(n\) 是偶数又有点麻烦,中心点不是整数。

我感觉我是对的,只是代码不会写,我去看看题解。

看了一眼,证明不想看了,感觉对了就是对了,好像就是这个做法。抄抄江丽怎么写的。

我绰江丽的写法是从大到小从外圈往里填。显然从小到大从里圈往外填是麻烦的,但是反过来就很简单。

然后实现上我有一个顺时针的写法。然后这里 vis 应该是按析构的方法初始化会好一点。

cin>>n;
int x=1,y=1,c=n*n;
while(true){
	vis[x][y]=1;
	g[x][y]=--c;
	if(y+1<=n&&!vis[x][y+1])y+=1;
	else if(x+1<=n&&!vis[x+1][y])x+=1;
	else if(y-1>=1&&!vis[x][y-1])y-=1;
	else if(x-1>=1&&!vis[x-1][y])x-=1;
	else break;
}
L(i,1,n)L(j,1,n)cout<<g[i][j]<<" \n"[j==n];
L(i,1,n)L(j,1,n)vis[i][j]=0,g[i][j]=-1;

但是这里在 \(n=4\) 时,从 \((3,1)\) 应该走到 \((2,1)\) 时,却走到了 \((3,2)\) 。经过测试单测和多测没有区别。打印路径发现确实是这样走的。

发现了,我们的顺时针走法的代码是四个 if ,这意味着当前能贪心地按顺时针走就顺时针走,而不是整体按顺时针走。

如果要整体按顺时针走,而不是贪心按顺时针走,应该改成四个 while 。改成下面那样就 nm 的对了。
注意我们在 while 开始前要先填一次。

cin>>n;
int x=1,y=1,c=n*n;
vis[x][y]=1,g[x][y]=--c;
while(c){
	while (c&&y+1<=n&&!vis[x][y+1])y+=1,vis[x][y]=1,g[x][y]=--c;
	while (c&&x+1<=n&&!vis[x+1][y])x+=1,vis[x][y]=1,g[x][y]=--c;
	while (c&&y-1>=1&&!vis[x][y-1])y-=1,vis[x][y]=1,g[x][y]=--c;
	while (c&&x-1>=1&&!vis[x-1][y])x-=1,vis[x][y]=1,g[x][y]=--c;
}
L(i,1,n)L(j,1,n)cout<<g[i][j]<<" \n"[j==n];
L(i,1,n)L(j,1,n)vis[i][j]=0,g[i][j]=-1;

3

https://codeforces.com/problemset/problem/2075/B

问:
给一个数组 \(a_1, a_2, \cdots, a_n\) ,每个数一开始都是蓝色。然后将 \(k < n\) 个数染成红色。

接下来,当存在一个数是蓝色,且它相邻那个数是红色,可以将这个数染成红色。直到最后所有数都是红色。

一个操作序列的代价为,前 \(k\) 次和最后一次染色的数的和。你需要构造一个序列使得这个和最大,只需要回答这个和。

思考:
直觉上是第前 \(k+1\) 个大数的和。

  • 一个相对好的情况,我们总可以染第前 \(k\) 个大的数,然后按某种顺序从两边向中间染,使得第 \(k+1\) 大的数被最后一次染到。
  • 存在一个相对不好的情况,比如前 \(k\) 个大的数染完后,第 \(k+1\) 大的数在两段,就没办法用上面的做法。但是我们可以先染这个数,少染一个前 \(k\) 大的数,那么这个少染的数则可以两边往中间染,使得最后一次染到。

看起来这个结论是对的。但是注意,我们考虑的是 \(n\) 的规模相对大的情况。

我看了一眼题解,他是给了具体证明的。我复刻一下。

\(k \geq 2\) ,我们把前 \(k+1\) 个数按位置顺序,标记下标为 \(p_1, p_2, \cdots, p_{k+1}\) ,一个可以使用的方案是前 \(k\) 个数染 \(p_1, p_3, p_4, \cdots, p_{k+1}\) ,最后有办法两边往中间染,使得 \(p_{2}\) 位置的数最后染到。这时候前 \(k+1\) 大的数显然是对的。这便是 \(k\) 的规模相对大的时候的具体证明。

如果经验足够,那么可以注意到,我们上述一个构造是开始剔除序列 \(p\) 中的一个,而剩下的依然不是空序列。这意味着序列至少大小为 \(2\) ,即 \(k>1\) 。这是隐藏条件。

\(k=1\) 呢。注意最后一个数的贡献一定是 \(max(a_1, a_n)\) ,那么实际上我们第一个数染最大的数就行。

hack: 最后一个数一定要么是第一个要么是最后一个,那么我们要找到具体是哪个数,然后在剩余的数中找到最大的,第一次染色染这个数。

下面是代码。希望能一次对。

view
cin>>n>>k;
L(i,1,n)cin>>a[i];
if(k==1){
	int x=1,mx=-1;
	if(a[1]>a[n])x=1;
	else x=n;
	L(i,1,n)if(i!=x){
		if(a[i]>mx){
			mx=a[i];
		}
	}
	cout<<a[x]+mx<<"\n";
}else{
	sort(a+1,a+n+1,greater<int>());
	sum=0;
	L(i,1,k+1)sum+=a[i];
	cout<<sum<<"\n";
}

总结:
我们直觉上总是能洞察到问题规模相对大的时候的结论,但直觉很容易忽略问题规模相对小的时候的情况。

4

https://codeforces.com/problemset/problem/2064/C

问:
给一个非 \(0\) 整数数组 \(a_1, a_2, \cdots, a_n\) 。一开始你有 \(0\) 个硬币,你将执行下述操作知道 \(|a|=0\) :

  • \(m\) 是当前的 \(|a|\) 。选择一个 \(i\) 使得 \(1 \leq i \leq m\) ,获得 \(|a_i|\) 个硬币,然后:
    • 如果 \(a_i < 0\) ,则替换 \(a\) 成为 \([a_1, a_2, \cdots, a_{i-1}]\)
    • 否则,替换 \(a\) 成为 \([a_{i+1},a_{i+2},\cdots,a_{n}]\)

回答你最终可以得到多少硬币。

\(1 \leq n \leq 2 \cdot 10^{5}\) \(-10^{9} \leq a_{i} \leq 10^{9}\)

思考:
如果只删前缀或只删后缀可以 dp 或者分块。

关键性质:
经过一些手玩。我们发现两个性质:

  • 如果选择了 \(i\) ,删除 \(i\) 的后缀,那么 \(j > i\) 部分的后缀应该优先删。
  • 显然可以证明总存在且只存在一个点作为分界线,左边是最后一段前缀,右边是最后一段后缀。

实际上最终问题是,左边一段是自左向右删完前缀,右边一段是自右像左删完后缀。

然后我们做一次预处理,删到 \(i\) 的前缀的最大代价 \(f\) ,删到 \(i\) 的后缀的最大代价 \(g\) ,枚举这个分界线 \(l\) 就行。答案是 \(max\{f_{l} +g_{l+1}\}\)

前缀的代价维护其实是,\(a_i > 0\)\(f_{i}=f_{i-1}+a_{i}\) ,否则 \(f_{i}=f_{i-1}\) 。后缀的 \(g\) 处理类似。

hack:
这个问题注意到允许空前缀或空后缀,维护答案 \(f_{i}+g_{i+1}\) ,考虑枚举 \(i=1 \sim n\) 显然不对。要么是允许空前后缀,考虑 \(0 \sim n\) ,要么是必须非空,考虑 \(1 \sim n-1\)

view
cin>>n;
L(i,1,n)cin>>a[i];
L(i,1,n){
	f[i]=f[i-1];
	if(a[i]>0)f[i]+=a[i];
}
R(i,n,1){
	g[i]=g[i+1];
	if(a[i]<0)g[i]-=a[i];
}
ans=0;
L(i,0,n){
	chkmax(ans,f[i]+g[i+1]);
}
cout<<ans<<"\n";
L(i,0,n+1)f[i]=g[i]=0;

5

https://codeforces.com/problemset/problem/2059/B

问:
给一个数组 \(a_1,a_2,\cdots,a_n\) ,一个偶数 \(k\) 。你需要将 \(a\) 划分成 \(k\) 个子数组,使得每个 \(a\) 中的元素都在一个子数组中。

然后,所有偶数下标的子数组 \(2,4,\cdots,k\) 拼接成数组 \(b\) ,然后 \(b\) 的末尾再加入一个 \(0\)

\(b\) 的代价定义为最小的下标满足 \(i \neq b\) 。确定我们经过操作后,能得到的最小的 \(b\) 数组的代价。

思考:
实际上 \(b\) 的代价就是 \(\min_{i=1}^{|b|} [i \neq b_{i}]\) ,我们要让这个 \(b\) 最小。题目让 \(b_{|b|} = 0\) 实际上是为了保证有解,最坏解会是 \(|b|\)

这里其实应该注意下 \(2 \leq k \leq n\) ,但在这里不会有太大影响。

我们尽可能让 \(i \neq b_i\)\(i\) 小,贪心的从小往大考虑。
如果能构造 \(b_1 \neq 1\) ,我们需要让第 \(2\) 个子段的第 \(1\) 个数 \(\neq 1\) ,而往后还有足够的空间划分 \(k-2\) 段。实际上往后存在 \(k-2\) 个数即可。(实际上往前也要存在一个数)
如果不能,则构造 \(b_2 \neq 2\) ,两种选择。

  • 一是让第 \(2\) 个子段的第 \(2\) 个数 \(\neq 2\) ,而往后有足够空间划分 \(k-2\) 段。实际上往前存在 \(1\) 个数,往后存在 \(k-2\) 个数即可。
  • 二是让第 \(2\) 个子段大小为 \(1\) ,而第 \(4\) 个子段的第一个数 \(\neq 2\) ,而往后有足够的空间划分 \(k-4\) 段。实际上往前存在 \(3\) 个数,往后存在 \(k-4\) 个数即可。
    接下来怎么办?我其实大概能想到我们可能要做一步“对值处理下标的邻接链表”,但是具体的做法我好像不想归纳了和手玩了,我想看题解了。

看一眼题解后的思考:
题解说的很简单啊,就几句话,感觉应该是个脑筋急转弯。

\(k=n\) ,分组方案唯一。答案唯一。
我们从小到大遍历 \(a\) 的偶数位置 \(i\) ,如果 \(i/2 != a_i\) 那么答案是第一个 \(i\)hack:\(a\) 中是 \(i\) ,在 \(b\) 中是 \(i/2\) ,我们需要的是在 \(b\) 中的位置),最终没找到,那么有 \(b_{|b|=0 \neq |b|}\) 答案是 \(|b|\) ,而这里的 \(|b|\) 只可能是 \(k/2+1\)

否则,答案不会超过 \(2\) 。下面是证明。

  • 首先一种方法是,让第二个子段不从 \(1\) 开始,而不管其他子段。如果能做到(这个位置往前有 \(1\) 个数,往后有 \(k-2\) 个数),那么答案是 \(1\)
    • 我们需要考虑这个位置的范围,实际上是 \(2 \sim n-k+2\)
  • 否则,就是说下标 \(2 \sim n-k+2\) 中全是 \(1\) 。那么让第一个子段是 \(a_1\) ,第二个子段是 \(a_2,a_3\) ,就有 \(2 \neq b_2\) ,那么答案是 \(2\)
view
cin>>n>>k;
L(i,1,n)cin>>a[i];
if(n==k){
	int ans=k/2+1;
	L(i,1,n)if(i/2*2==i){
		if(a[i]!=i/2){
			ans=i/2;
			break;
		}
	}
	cout<<ans<<"\n";
}else{
	int win=0;
	L(i,2,n-k+2)if(a[i]!=1){
		win=1;
		break;
	}
	if(!win)win=2;
	cout<<win<<"\n";
}

总结:
一些比较板而且没有考虑的地方是:

  • 我没有考虑 \(k=n\) 的不可调整的特殊情况。
  • 也没有考虑是否总能归纳到 \(k=2\) 的特殊情况。这题里看起来不太能行。

注意递降法(反复约束范围)在下标上的使用:
如果一个条件很可控或者范围很大,那么最好确定出他的范围,这样可以很好的使用递降法判断其他情况。
我想到了可以先处理 \(1\) ,但是没确定出他的范围,如果确定出来了。那么它的矛盾情况显然是非常可控的(当时我也意识到了这点,但是不知道具体该怎么处理)。
我之前用递降法的时候通常都是在值上而非下标上用的……需要注意到下标上也很好用。

6

https://codeforces.com/problemset/problem/2049/B

问:
给一个字符串 \(s=s_1 s_2 \cdots s_n\)\(p,s,.\) 三种字符组成。\(s\) 可以以如下方式确定 \(p\)

  • \(s_i=p\)\([p_1,p_2, \cdots, p_i]\) 是一个排列;
  • \(s_i=s\)\([p_i,p_{i+1},\cdots,p_{n}]\) 是一个排列;
  • \(s_i=.\) ,没有额外限制。

\(s\) 是否能构造出至少一个 \(p\)

思考:
想了一下,如果前缀和后缀都存在至少一个,那么任意前缀和后缀至少应该是相交的。
更严格地说,如果前缀和后缀至少存在一个,那么任意前缀和后缀应该是严格包含的。
\(.\) 号很可能不会影响什么东西。

然后呢?然后我就不想思考了。看题解吧,科学地快速刷题。

看了一眼题解后的思考:
有个勾吧思考,题解直接给了做法。然后我可以从它的做法总结一些思考。

如果 \(s,p\) 有不存在的,容易归纳 \(s\) 的正确性。
否则 \(s,p\) 都存在。如果他们的出现次数都 \(\geq 2\) ,显然总存在一个前缀和一个后缀是不严格包含的关系。
当且仅当 \(p\) 只出现一次,且是最后一个位置。或者 \(s\) 只出现一次,且是第一个位置。存在答案。这两种情况意味着这个前后缀代表整个字符串。

详细的实现我觉得可以写在注释里

view
cin>>n>>s;
if(count(s.begin(),s.end(),'s')==0||count(s.begin(),s.end(),'p')==0){ // 要么是只存在 s 或者 p ,显然对 
	cout<<"YES\n";
}else if(count(s.begin(),s.end(),'s')>1&&count(s.begin(),s.end(),'p')>1){ // 要么是 s 或 p 出现都 > 1 ,显然错 
	cout<<"NO\n";
}else if((count(s.begin(),s.end(),'s')==1&&s[0]=='s')||(count(s.begin(),s.end(),'p')==1&&s[n-1]=='p')){
	// 否则,s 和 p 都出现过,且有一个只出现了 1 次。
	// 讨论哪个出现了 1 次,然后是单次出现的前后缀需要覆盖所有段。
	// 这时显然对 
	cout<<"YES\n";
}else{
	// 再否则,依然是 s 和 p 都出现过,且有一个只出现了 1 次。但并非单次的前后缀覆盖所有段。显然错。 
	cout<<"NO\n";
}

总结:

  • 考虑前后缀的覆盖问题,当他们覆盖整个区间,即非真前后缀,是需要考虑的特殊情况。
  • 任意真前缀和真后缀一定不是严格包含的。
  • 两个不严格包含的区间不可能同时都是排列。

7

https://codeforces.com/problemset/problem/2031/C

问:
\(n\) 个面包,编号为 \(1 \sim n\) ,有编号 \(1 \sim 10^{6}\) 种馅料可选。每个面包可以加入一种馅料,限制条件:

  • 没有一种馅料只使用一次。即要么不使用,要么至少使用两次。
  • 任意两个相邻位置的面包 \(i,j\) 若满足加入的馅料相同,则应当满足 \(|i-j|\) 是完全平方数。

给定 \(n\) ,询问是否能构造一种加入馅料的方式,如果能则按照面包编号 \(1 \sim n\) 的给出。

题解:

当作背板吧。

显然可以考虑 \(2 \mid n\) 的情况,我们可以构造 \(1,1,2,2,3,3,\cdots\) 以使得条件满足。那么接下来考虑 \(n\) 是奇数的情况。

如果 \(n\) 是奇数,可以证明总存在 \(3\) 个位置 \(x < y < z\) ,他们的馅料编号一样。

我们断言 \(x-y,z-y,z-x\) 都是完全平方数的情况下,最禁的约束是 \(z-x=5^{2}=25\) 。而 \(y\) 的位置要满足 \(3^{2}+4^{2}=5^{2}\) 。这不难证明。

最紧的情况是 \(x=1,z=26\) 。如果 \(n\) 比他更紧肯定做不到,即 \(n < 26\)
显然 \(n=26\) 可以用偶数做法,所以我们要考虑 \(n=27\)

注意,严谨地说,\(n < 27\) 的奇数我们做不了。

如果我们认为 \(26\) 是最后一个位置。我们先把三个位置放完,\(a_{1}=1,a_{10}=1,a_{26}=1\)
\(a_1\)\(a_10\) 之间有 \(8\) 个空位,可以两两填入馅料编号。显然 \(a_{10}\)\(a_{26}\) 之间有 \(15\) 个空位,它是 \(< 27\) 的奇数,做不了。

那么我们考虑 \(27\) 是最后一个位置。我们先把三个位置放完,\(a_{1}=1,a_{10}=1,a_{26}=1\) 。然后让 \(a_{27}=2\)\(a_{11}=2\) 。那么 \(a_{1}=1,a_{10}=1,a_{11}=1,a_{26}=1,a_{27}=2\) 是显然合法的。注意 \(a_1,a_{10}\) 之间 \(8\) 个空位,\(a_{11},a_{26}\) 之间 \(14\) 个空位。这也是可以两两构造的。

于是 \(n=27\) 可以构造一组解是 \(1,2,2,3,3,4,4,5,5,1,2,6,6,7,7,8,8,9,9,10,10,11,11,12,12,1,2\) 。(已hack
对于 \(>27\) 的奇数,我们可以从 \(13\) 开始两两填入。(上面的被 hack ,那么这里应该从 \(14\))。

hack:
我们对于 \(<27\) 的空位两两填数时,显然还剩下 \(8+14=22\) 个空位,这意味着我们可以从 \(3\) 开始到 \(13\) 结束填入 \(11\) 个数。而我上面的例子从 \(2\) 开始填,显然错了。

\(n=27\) 的一组解是

\[1,3,3,4,4,5,5,6,6,1,2,7,7,8,8,9,9,10,10,11,11,12,12,13,13,1,2 \]

下面是实现。

view
cin>>n;
if(n%2==1&&n<27)cout<<-1<<"\n";
else if(n%2==0){
	L(i,1,n){
		cout<<(i+1)/2<<" \n"[i==n];
	}
}else{
	int val=14,f=0;
	L(i,1,n){
		if(i<=27)cout<<v[i]<<" \n"[i==n];
		else {
			cout<<val<<" \n"[i==n];
			val+=f;
			f^=1;
		}
	}
}

8

https://codeforces.com/problemset/problem/2026/B

问:
有编号从 \(0 \sim 10^{18}\) 的格子开始全是白色。你可以选择 \(i \neq j, |i-j| \leq k\) 并且把这两个本来是白色的格子染成黑色。给 \(n\) 个元素 \(a_1,a_2, \cdots, a_n\) ,你必须将这些编号的格子染成黑色,并且至多一个其他格子被染成黑色。

询问最小的 \(k\)

思考:

\(n\) 是偶数,一个显然对的方法是看两两 \(a_i,a_{i+1}\) 的绝对值。然后维护出最小的 \(k\)
比如 \(a_2 - a_1, a_4 - a_3, a_6 - a_5, \cdots\) 。(我们一直假设 \(a\) 有序)

\(n\) 是奇数,我们要构造一个额外的数 \(x\) 加入 \(a\) 。按偶数的方法做也是答案。难点在于这个 \(x\) 怎么找。
我就不去想了。看一眼题解。

噢注意这题的 \(n \leq 2000\) ,但值域很大。但我依然没办法很快反应出来。

我想到了一个情况是,如果 \(n\) 是奇数,那么总存在一个点要和 \(x\) 配对,而剩下的偶数个点两两配对。要么是左边奇数个点右边奇数个点,要么是左边偶数个点右边偶数个点,看起来是第二种情况更优。这样我们可以预处理一个前后缀。

假设在一组数据下,我们可以存在“一个点和 \(x\) 配对,左边奇数个点,右边奇数个点”是最优的。那么我们调整成两两配对的状态会更优。矛盾得出。

比如 \([l_1,r_1],x,y,[l_2,r_2]\) ,且 \(r_1 - l_1 + 1, r_2 - l_2 + 1\) 都是奇数。那么实际上我们让 \(r_1\)\(x\) 配对、\(y\)\(l_1\) 配对,比 \(x,y\) 配对然后两边要选一个越过 \(x,y\) 去配对,更优。

然后查了一下,确实有线性做法。好像看起来我的做法就是对的。

具体来说,当 \(n\) 是奇数。

我们处理 \(f_{i}\)\(\leq i\) 的位置,尽可能的两两配对 \(a_{l} - a_{l-1}\) 维护出的最大值,显然我们要在 \(l\) 是偶数的时候对 \(f_{l}\) 进行更新,而 \(f\) 自身可以前缀继承。

处理 \(g_{i}\)\(\geq i\) 的位置,尽可能的两两配对 \(a_{l+1} - a_{l}\) 维护出的最大值,显然我们要在 \(l\) 是偶数的时候对 \(g_{l}\) 进行更新。而 \(g\) 自身可以后缀继承。

最后我们枚举奇数位置 \(i\) ,则 \(d=max(f_{i-1},g_{i+1})\) 就是答案,因为我们可以选择 \(x=i-1,i+1\) 这两个位置,让 \(i\)\(x\) 配对。

存在一种坏的情况是,\(i-1,i+1\) 的位置分别是前缀和后缀里的点,而我们不能选择选过的点。

实际上,我们没有必要关注 \(i-1,i+1\) 两个位置,而是 \([i-d,i+d]\) 这段区间是否存在一个没有用过的点。

如果是朴素的情况 \(d=1\) ,那么我们可以把右边的配对点全部平移一下,总能让答案还是 \(d\)

如果 \(d > 1\) ,且 \([i-d,i+d]\) 的点都被用过了,那么把 \([i-d,i+d]\) 的配对点都平移一下,则第 \(i+d\) 位置上的点没有配对,则他能往又找到一个 \(\leq d\) 距离的点且一定存在空位,否则他不是连续段的端点。

因为没有更优的情况,而我们能调整到一个不会更坏的情况。所以这是答案。

那么答案是枚举奇数位置 \(i\) ,然后维护 \(max(f_{i-1},g_{i+1})\) ,用最小的更新答案。

最后还要注意,\(n\) 为奇数时,上面的做法只适用于 \(n\) 相对大的情况,不难发现最小是 \(n \geq 3\) ,所以 \(n=1\) 需要直接输出答案 \(1\) (我们可以发现)。

那么实际上 \(O(n)\) 可以做到解决这题。

实现:

view
cin>>n;
L(i,1,n)cin>>a[i];
if(n%2==0){
	ans=0;
	L(i,1,n){
		chkmax(ans,a[i+1]-a[i]);
		i++;
	}
}else if(n==1){
	ans=1;
}else{
	L(i,0,n+1)f[i]=g[i]=0;
	L(i,1,n){
		f[i]=f[i-1];
		if(i%2==0){
			chkmax(f[i],a[i]-a[i-1]);
		}
	}
	R(i,n,1){
		g[i]=g[i+1];
		if(i%2==0){
			chkmax(g[i],a[i+1]-a[i]);
		}
	}
	ans=1LL<<60;
	L(i,1,n){
		chkmin(ans,max(f[i-1],g[i+1]));
		i++;
	}
}
cout<<ans<<"\n";

总结:

  • 这题本质上是找一个点,割开前后缀能分别处理。
  • 这题的应用的调整思维其实还是挺常见的。先考虑一个合适的情况,可能存在冲突,但是我们又容易调整成不冲突的状态,且依旧符合约束。

9

真是有趣的结论题。
https://codeforces.com/problemset/problem/2023/A

问:
\(n\) 个数组 \(a_1, a_2, \cdots, a_n\) 。每个数组 \(a_i\) 是有序的 \([a_{i,1}, a_{i,2}]\) 。你需要将 \(a\) 排序,比如下标 \(1,2,\cdots,n\) 映射到了 \(p_1,p_2,\cdots,p_n\) 。那么拼接后的 \(2n\) 个有序数就是 \(a_{p_1,1},a_{p_1,2},a_{p_2,1},a_{p_2,2},\cdots,a_{p_n,1},a_{p_n,2}\)

实际上,我们希望最后 \(2n\) 个数的逆序对最少。你需要输出排序后的这 \(2n\) 个数。

思考:
其实 \(a\) 内部是不能变的,而要按最优方式排序 \(a\) ,显然就是考虑邻项交换,这个方法也可以归类到啥微扰、调整,当然原理是构造偏序。然后我考虑邻项的话,我会去考虑 \(a_{i,1},a_{i,2},a_{i+1,1},a_{i+1,2}\) 这四个数分两组互相的大小比较,然后去分类讨论。可能我要错很多发才能讨论出来或者猜到结论。

实际上真实的结论就是,考虑 \(a_{i,1}+a_{i,2}, a_{i+1,1}+a_{i+1,2}\) 的大小比较。

这个证明思路非常关键:如果左边两个数的元素之和大于右边两个数的元素之和,那么我们交换左右两个位置,整个数组的逆序对不会增加。如果这是真的,那么这已经构造除了偏序。至于为什么是真的,可以分类讨论证明。
于是这个结论就可以背下来了。

实际上有真实的分类讨论做法,我们钦定左值的最大值最小值是 \(l_{mx},l_{mi}\) ,右值的最大最小值是 \(r_{mx},r_{mi}\) 。重载 sort 或者重载结构体都行。

我们的核心目的是,只要我们能构造出偏序,那么我们就会对。

如果 \(l_{mi} \neq r_{mi}\) ,那么不妨让 \(l_{mi} < r_{mi}\) ,这对偏序来说是好的。否则让 \(l_{mx} < r_{mx}\)
当然,我们先讨论 \(l_{mx} \neq r_{mx}\) 也是一样,也能构造出偏序,这就是偏序的魅力。

实现:
代码就不用放了。
代码核心就是对 \(a\) 实现个 pair ,然后重载一下排序啊。

可以 \(LHS.1+LHS.2 < RHS.1+RHS.2\) 的结论。也可以讨论最大最小值。

总结:

  • 收获了一点结论。
  • 之前我只知道所谓的偏序构造为什么对。以及知道了结构上的构造技巧,考虑邻项交换(就是当前相邻的两个数,然后前面存在前缀)。
  • 但是之前不知道邻项交换应该以何种视角构造偏序,现在知道了——如果邻项交换不会更坏,那么就可以定义偏序。而不必执着于要更优才能定义偏序。

10

https://codeforces.com/contest/2021/problem/C1
https://codeforces.com/contest/2021/problem/C2

问:
\(n\) 个标号为 \(1 \sim n\) 的人演讲 \(m\) 张幻灯片,每张幻灯片可以被一个人演讲。

有一个数组 \(a = a_1, a_2, \cdots, a_n\) 代表起初 \(n\) 个人的排序,他们从前往后轮流演讲当前的幻灯片。当队首的人演讲完幻灯片后,你可以把他移动到队列中的任何位置,但不改变其他成员的相对位置。

还有一个数组 \(b = b_1, b_2, \cdots, b_n\) ,第 \(i\) 张幻灯片被认为是 \(good\) 的,如果编号为 \(1,2,\cdots,b_i\) 的其中一个人演讲了它。

\(q\) 次对 \(b\) 的更新。第 \(i\) 次更新,他会选择一张幻灯片的编号 \(s_i\) ,一个人的编号 \(t_{i}\) ,然后执行 \(b_{s_i} = t_i\) 。注意更新的影响是持续的。

简单版本数据: \(1 \leq n,m \leq 2 \cdot 10^{5}\)\(q=0\)\(1 \leq a_i \leq n\)\(1 \leq b_i \leq n\)
困难版本数据:\(1 \leq n,m \leq 2 \cdot 10^{5}\)\(0 \leq q \leq 2\cdot 10^{5}\)\(1 \leq a_i \leq n\)\(1 \leq b_i \leq n\)\(1 \leq s_i \leq m\)\(1 \leq t_i \leq n\)

思考:

posted @ 2025-04-02 08:57  03Goose  阅读(83)  评论(0)    收藏  举报