【题解】CSP2019-Day2

这次考试的状态非常渣……连T1都没有想出来……

考试考完了直接就自闭了,所以没有写题解。

Emiya家的饭(meal)

当时考场上想出来是一个类似容斥的东西,但是状态不好想不下去了……

不过,这题确实是一道标准的提高组难度的计数DP题。需要的想法有:反向计数与主元法。

想法

由于“每种主要食材至多在一半的菜中被使用”这个条件比较难刻画,考虑它的相反条件:“存在一种主要食材,在超过一半的菜中被使用”。

这个相反条件看起来比之前的条件好了一点,但是感觉还是比较难刻画……

我们考虑“超过一半”这个限制有什么性质。显然,在任何方案中,至多有一种主要食材被使用了超过一半次。

这样,我们就想到枚举这一种食材,并对不合法的方案进行计数。

朴素DP

由于每道菜的烹饪方法互不相同,我们考虑沿着烹饪方法这一维进行DP。

枚举使用次数超过一半的食材\(x\),设\(f_{i,j,k}\)表示考虑前\(i\)种烹饪方法,已经选了\(j\)种食材,且食材\(x\)恰被使用了\(k\)次的方案数。设\(S_i=\sum_ja_{i,j}\),则转移式为:

\[f_{0,0,0} = 1 \\ f_{i,j,k} = f_{i-1,j,k}+(S_i-a_{i,x})f_{i-1,j-1,k}+a_{i,x}f_{i-1,j-1,k-1} \]

最后的答案即为\(\sum_x\sum_{j<2k}f_{n,j,k}\)

转移是\(O(1)\)的,但状态数是\(O(n^3)\)的。又因为要枚举食材\(x\),总复杂度是\(O(mn^3)\)的,会TLE。

优化

DP常见的优化策略是缩减状态或者用数据结构优化转移。然而,本题似乎没法用数据结构优化……

考虑如何缩减状态,显然只能够尝试将\(j,k\)缩成一维。由于我们最后只需要保证\(j<2k\)。将\(j\)\(k\)换成一个存储\(2k-j\)的新的下标即可。这个下标的意义是第\(x\)种食材的使用次数减去其他食材的使用次数的值。

我们稍微改动一下上面的转移即可:

\[f_{0,0,0} = 1 \\ f_{i,j,k} = f_{i-1,j,k}+(S_i-a_{i,x})f_{i-1,j+1}+a_{i,x}f_{i-1,j-1} \]

注意在程序实现中,\(k\)可能是负的,需要整体加上一个\(\ge n\)的常数。

抽象一下以上的思路,设\(P_i\)表示满足第\(i\)种食材的使用次数不超过一半的方案,则答案即为\(|\bigcap P_i|\)

我们发现了一个特点:\(\overline{P_i}\cap\overline{P_j}=\varnothing\)。于是,

\[|\bigcap_i P_i|=U-|\bigcup_i \overline{P_i}|=U-\sum_i|\overline{P_i}| \]

这是容斥原理的思路的另一种应用(还有一种:转化为一堆集合交的和)。

划分(partition)

初步

看到这题,肯定会首先想到DP啦~

可以设\(f_{i,j}\)表示DP到了\(i\),最后一段是\([i,j]\)的最小平方和。设\(s_{l,r} = \sum_{i=l}^ra_i\),则转移如下:

\[f_{i,1}= s_{1,i}^2\\ f_{i,j} = s_{j,i}^2+\min_{0\le k<j,s_{k,j-1}\le s_{j,i}}f_{j-1,k} \]

若采用朴素实现,总复杂度\(O(n^3)\)

转移的优化

只需要快速求得\(\min_{l\le k\le j-1}f_{j-1,k}\)即可。

由于\(s_{i,j}\)随着\(i\)的增加而增加,所以\(l\)随着\(i\)的增加而减少,故对一个\(j\),只需要枚举所有\(i\),并维护\(l\)即可。

考场上:

由于\(s_{k,j-1}\)随着\(k\)的增加单调递减,预处理\(s_{k,j-1}\)\(g_{l,r}=\min_{l\le k\le r}f_{r,k}\)之后,可以用二分得到\(k\)的最小取值\(l\)并得到答案。

最后是\(O(n^2\log n)\)的。

总复杂度是\(O(n^2)\)的。

再进一步

我们之前的DP已经很难优化了:状态数难以缩减,再怎么优化也止步于\(O(n^2)\)

此时的一个常见思路是寻找决策的性质(如决策单调性)。

我们考虑寻找\(j,k\)的性质。通过打表我们可能可以找到这一个规律:

对于最优解\(ans_i = \min_j f_{i,j}\),其决策点\(j\)是最大的。同样,对其它的决策点也满足这个最大性。

从直观上来看,这个结论比较说得通:若每一个连续段的平方和都比较小,那么其总和也应该比较小。我们尝试证明这个结论。

\(A_1\ge\dots\ge A_{l_a}\ge A_{l_a+1}=0\ge \cdots\)为满足每一个划分点都尽量大,段数尽量多的划分(\(A_i\)表示第\(i\)段的和)。考虑一个与\(A\)不同的划分\(B_1\ge \dots\ge B_{l_b}\ge B_{l_b+1}=0\ge\cdots\)。显然,有\(\sum_{i\le k}A_i\le \sum_{i\le k}B_i\)

我们只需要证明任何满足\(\sum_{i\le k}A_i\le \sum_{i\le k}B_i\)的(不一定是合法划分的)\(B\)不是更优的即可。

\(\sum_{i\le k}A_i= \sum_{i\le k}B_i\),则\(B=A\)\(B\)不比\(A\)更优。

\(SA_{i}=\sum_{k\le i}A_k\)。考虑最小的\(k\)使得\(SA_k\ne SB_k\),显然\(SA_k<SB_k\)。考虑最小的\(l\)使得\(SA_l=SB_l\)。由于\(A_k<B_k\)\(\sum_{i=k}^l(A_i-B_i)=0\),故存在\(i\ge k\)使得\(A_i>B_i\)。有\(B_k>A_k\ge A_i>B_i\),故\(B_k-B_i\ge 2\)\(B\)\([k,i]\)间的值的跨度至少为\(2\)

考虑\(B\)的最靠后的最大值和最靠前的最小值,将他们分别\(-1\)\(+1\),可以得到新数列\(B'\),仍然满足\(B_i\ge B_{i+1}\)\(\sum_{i\le k}A_i\le \sum_{i\le k}B_i\)。重复操作后,可以使得\(B\)变为\(A\)(因为\(i\)在每一“堆”操作后递减)。

算法

维护\(f_i\)表示以\(i\)为结尾的,最小的最后一段的和:

\[f_{i} = \min_{j<i,f_j\le s_{j+1,i}}s_{j+1,i} \]

由于上式等价于求最大的\(j\),满足\(f_j+s_j\le s_i\),单调队列(+指针)维护递增的\(\min_{k\le j<i}f_j+s_j\)即可。

实现

哇……这题除了要写高精度以外它甚至还卡了空间和时间……最多只能开\(1024*1024*1024/(4*10^7)/8=3\)long long数组……。写朴素的高精度甚至会TLE……需要用奇怪的优化方法。

注意到本题用__int128是存的下的,我们考虑用两个long long来保存答案:

typedef long long ll;
const ll P1 = 1e9, P2 = 1e18;
pair<ll,ll> mul(ll a, ll b) {
  ll a1 = a/P1, a0 = a%P1;
  ll b1 = b/P1, b0 = b%P1;
  ll ans0 = a0*b0, ans1 = a1*b1;
  ll tmp = a1*b0+a0*b1;
  ans0 += tmp%P1*P1;
  ans1 += tmp/P1;
  while (ans0 >= P2) {
    ans0 -= P2;
    ans1++;
  }
  return make_pair(ans1, ans0);
}

参考

树的重心(centroid)

考场上这题我只拿到了暴力分……(即使是75分也算比较可观了……)

(本题的各种各样的)算法:换根+倍增/(以重心定根+)数据结构/数学(推式子)

思路

本题的部分分启发我们去找性质:

  • 链的部分分启发我们去考虑树链剖分
  • 二叉树的部分分启发我们去考虑以重心为根的情况。
  • 由于要同时考虑子树与外子树的情况,这启发我们想到换根法

性质:

  1. \(u\)在根节点所在的重链上。
  2. \(u\in son_{rt}\)\(v\)\(u\)的重心,则\(rt\)的重心为\(v\)的祖先。
  3. \(u\)为重心,只有\(u\)的重儿子(或父亲)有可能是重心。

计数方法:

  1. 对于每一个点,我们计算它作为重心的次数:这会导致实现偏向数据结构(BIT、可持久化线段树、树状数组等)。
  2. 对于每一个分割,我们去找两课树的重心:这回导致实现偏向图论类方法(树链剖分、找重儿子、倍增等)。



方法1.0:考虑点

考虑一个点\(u\),我们想要知道,它会成为几次重心。

将树在\(u\)处定根,则移除一条边相当于删除一个子树。

设其最大的子树大小为\(S=siz_w\),则可以分为两种情况:

  1. 删去的子树\(tree_v\not\subseteq tree_w\):则当且仅当\(S\le \lfloor (n-siz_v)/2\rfloor\),即\(siz_v\le n-2S\)时,\(u\)成为重心。(当\(0<n-2S\)时有解)
  2. 删去的子树\(tree_v\subseteq tree_w\):设除了\(tree_w\)最大的子树大小为\(T\),则当且仅当\(T\le \lfloor(n-siz_v)/2\rfloor\)\(S-siz_v\le \lfloor(n-siz_v)/2\rfloor\),即\(2S-n\le siz_v\le n-2T\)\(u\)成为重心。(总是有解)

因此,我们考虑处理出以\(u\)为根,各个儿子的\(\{siz\}\)的可重集合的情况,再查询一段区间内的\(siz\)的个数即可。

维护方法

可以用可持久化线段树(可能可以线段树合并?或者将所有查询离线化?)维护。特别地,\(fa_u\)的情况,相当于\(tree-anc_u-tree_u\)\(\{u\}+anc_u-\{1\}\)取反。

tle.cpp #775715

这玩意细节又多,常数又不知道大到哪里去(差点超时),不推荐

类似方法

方法1.1:考虑点,但是以重心为根

我们先找到重心\(rt\),并以它作为根。这样和随意选根有什么区别呢?

为了方便叙述,对于一次删边\((x,y)\)的操作(不妨设\(y\in son_x\)),称\(tree_y\)为“子树”,\(tree_{rt}-tree_y\)为“剩余部分”。

性质

性质1:对\(w\in son_{rt}\),如果删掉了\(tree_w\)内的一条边\((u,v)\),则剩余部分的重心一定不在\(tree_w\)内。

​ 证明:因为\(2s_w\le n\),则\(2(s_w-S)\le n-2S< n-S\),故\(2(n-s_w)>n-S\),故\(w\)不可能为重心。实际上,我们可以得到一个推论:

推论2:若\(x\ne rt\)为重心,则删去的边\((u,v)\)一定不在\(tree_x\)内。

阐述:

  1. \(x\)为子树的重心,则删去的边一定为\(x-rt\)路径上的某一条边,不在\(tree_x\)内。
  2. \(x\)为剩余部分的重心,则由性质1,可知删去的边一定不在\(tree_x\)内。

因此,对一个点\(u\ne rt\),他作为重心仅当删去了一条边\((x,y)\),且:(设树减少的大小为\(S\)

  1. \((x,y)\)不在\(tree_u\)内。
  2. \(\forall v\in son(u),s_v\le (n-S)/2\),且
  3. \((n-S-s_u) \le (n-S)/2\)

\(n-2s_x\le S\le n-2g_x\)

对于根的情况,可以另外判断。

可以用树状数组维护,具体见这篇题解

参考

方法2:考虑边

考虑一种删边情况,我们需要快速求出,划分后的所有重心。

考虑如何求一棵(子)树的重心:若\(u\)不为重心,则重心一定在其重儿子的子树内,从根一直跳重儿子即可。

做法

我们考虑一个划分\((u,v)\),不妨设\(dep_u>dep_v\)

考虑如何求\(tree_u\)的重心:我们从\(u\)出发,一直向重儿子跳,跳到找到重心(即,重儿子的子树大小不超过原树的一半)为止。

考虑如何求出\(tree-tree_u\)的重心:我们从\(v\)出发,一直向上跳到父亲的子树不再是重子树,再一直向重儿子跳即可。(注:可以通过换根转化为上一种情况)

实现

第一次遍历时,预处理出重儿子/次重儿子,以及向重儿子的倍增数组。

第二次遍历时,用换根DP的思路,若当前点\(u\)已经为根,枚举\((u,v)\)边,将它断掉,此时只有\(u\)的重儿子发生了改变,处理掉倍增的部分即可。求出\(v\)的联通块的中心的方法类似。递归之前把\(v\)转化为根即可。

更进一步地,可以在将\(v\)转化为根之后再处理\(u\)的情况。

实现中,注意到,当\(u\)为根时,不需要保证\(u\)的信息的正确性,只需要保证\(v\ne u\)的信息的正确性即可,因为我们实际上只需要对某一个\(v\)求出它的答案并换根,注意换根时重新计算/恢复\(u\)的信息即可。(这样做有一个好处:不需要重新计算\(v\in son_u\)以及\(v=fa_u\)的答案)

脚注

short.cpp #776128

实现时犯的智障错误

  1. 判断\(u\)的重儿子是否是重心的条件写错了。
  2. 忘记在每次dfs1之前清零fg
  3. 判断什么时候应该向下跳的u不代表根节点……应该在最开始做一个copy避免混淆……

参考

某神奇的\(O(n)\)做法

就如之前所说的,可以在\(O(n)\)计算出所有子树的重心,但是很难对去掉子树的部分找到一个重心单调移动的计算序列……

以重心为根,考虑删去的边在哪棵子树内:

  1. 不在重儿子的子树内:则枚举所有可能的子树大小,在根所在的重链上面走就好了。
  2. 在重儿子的子树内:
    1. 重儿子仍然为重儿子:此时,根仍然为重心。
    2. 次重儿子变为了重儿子:在次重儿子的重链上走即可。

这样就用纯图论方法完成了这题。

参考

posted @ 2021-05-20 20:52  frank3215  阅读(85)  评论(0编辑  收藏  举报