杂题选做(2)

小思想

主要记录没那么大份的东西。

1

我忘了题号了,核心大概就是,有若干个可达的集合每个集合都是 \(0,1,2\) 的一个子集,你需要每个集合选出一个数,问它们的和模 \(3\) 意义下的可达性。考虑取调整量,先把每个子集取最小数并求和,接着求出每个集合能给调整贡献多少,能够 \(+1\) 当且仅当有至少一个集合有 \(+1\) 的调整量或者有至少两个集合有 \(+2\) 的调整量,能够 \(+2\) 类似。

2

link

我好像是,不会这个 \(O(nD)\) 做法。这个题有一个神奇的方法,考虑将左右的影响分离,我们可以将平面直角坐标系转 \(45\) 度,即让 \((x,y) \to (x+y,x-y)\),此时两边的分离,变成求解两个一维问题:若干 \(d_i\),对于 \(x\)\(y\) 分开决定系数,使得最后的和为 \((A+B,A-B)\)。考虑如何转化为我们会做的 01 背包,假设我们要让 \(\sum c_i d_i=K,c_i \in \{-1,1\}\),我们想办法改变 \(c\) 的值域,将等式两边同时加上 \(\sum d_i\),则变成 \(\sum c_i d_i=K+\sum d_i,c_i \in \{0,2\}\),发现可以两边同时除以 \(2\)\(\sum c_i d_i=\frac{K+\sum d_i}{2},c_{i}\in \{0,1\}\),跑 01 背包即可,存在性背包考虑 bitset 优化可以做到 \(O(\frac{n^2D}{w})\)

图论

CF2097B

link

随机跳题的,好像一眼秒了,也不知道记了有啥意义。首先你考虑,一个点 \(p_{2i-1}\)\(p_{2i+1}\) 的方案数完全取决于是否在同一行/列,同行/列就是一种,否则有两种,于是现在就要考虑这个不能重复的限制了,考虑图论建模,将备选点离散化或者哈希啥的,于是转化为了一种将边的定向问题,简单分讨得知对于树枚举根每种都可以合法,方案数为 \(n\),基环树的话,判掉自环的 \(1\),否则就是 \(2\),其余图显然无解,注意到整个东西并不联通,乘法原理就做完了。

CF109C

link

我完蛋了,做了 \(20min\) 只会 \(O(n^2)\) 人麻了。查看了题解,我们考虑转化一下判定有幸运边的条件,我们把所有幸运边断开之后,两个点之间有幸运边当且仅当不在同一个联通块里,我们记 \(u \in s_u\),于是要算的就是:

\[\sum_{i \ne j \ne k}[s_i \ne s_j][s_i \ne s_k] \]

考虑固定 \(i\),算对应的 \(j,k\)

\[\sum_{i} \sum_{j \ne k \ne i} [s_i \ne s_k][s_i \ne s_j] \]

我们记 \(|s_u|=c_u\),此时 \(j,k\) 要从剩下的 \(n-c_i\) 个里面选, 因此方案数就是 \((n-c_i)(n-c_i-1)\),直接求是 \(O(n)\) 的,考虑并查集求出联通块大小分别计数即可。

DP

CF1437F

link
考虑合法排列的基本性质,就是你的最大值一定是大于 \(2\) 倍其余值的,我们由此意识到最大值很关键,所以把它加入 DP 状态,定义 \(f_{i,j}\) 表示你考虑了 \(i\) 个渔夫(不确定是哪 \(i\) 个),并且当前的最大值为 \(a_j\) 的方案数。考虑转移,也就是我们再加入一个,分类讨论:

  • 当前加入的是最大值 \(a_j\),前面的相对于 \(a_j\) 小鱼都可以选入,因此就是 \(\sum_{2a_k \le a_j}f_{i-1,k}\)
  • 当前加入的是相对于 \(a_j\) 的小鱼,我们设 \(a_j\) 的小鱼有 \(c_j\) 个,可以用二分预处理求出。前 \(i-1\) 个渔夫的鱼中,有一个是 \(a_j\),剩下的我们根据一开始的性质得知都是小鱼,所以就有 \(i-2\) 的小鱼,转移就是在 \(c_j-i+2\) 里面任选,转移就是 \(f_{i-1,j}\times (c_j-i+2)\)

这样复杂度是 \(O(n^3)\) 的,根本过不了,注意到主要瓶颈在于求和,我们将 \(a\) 排序后,\(k\) 必然是 \(a\) 的一个前缀,考虑前缀和优化即可,转移时顺手维护。因为排序了,所以答案就是 \(f_{n,n}\),别忘了判一下原序列的最大值唯一性。

CF1334F

link
菜完了,一开始不会,经过提示貌似会了结果想的复杂度是假的第二天才发现。

你考虑我们定义状态 \(f_t\) 表示最终 \(b\)\(a_t\) 结束,你处理外 \(1\)~\(t\) 的最小花费,我们不妨设 \(b_j=a_t\)

考虑 \(b_{j-1}\) 如何向 \(b_{j}\) 转移,假设 \(k<t\)\(a_k=b_{j-1}\),(这里加入 \(j\) 表示第 \(j\) 轮的,实际实现中并不需要)于是 \(f_{t,j}=\min\{f_{k,j-1}+w(k+1,t-1,b_{j-1})\}\)

然后 \(w(l,r,v)\) 表示为了合法你要删的东西 \(p\) 的和,具体的,对于所有 \(p_x<0\) 的删去,\(a_x>v\) 的删去。解释一下 \(w\) 里为什么是 \(b_{j-1}\),你需要保证到 \(j\) 的过程不能出现前缀最大值,所以必须 \(\le b_{j-1}\),这里肯定是有 \(<b_j\) 的限制的,但是因为 \(b_{j-1}<b_j\),所以前者限制更紧,取前者即可。

现在发现主要的问题是求 \(w(l,r,v)\),我们分类讨论一下,你发现对于一个 \(x\),它对 \(w\) 的贡献就是一个形如 \([a_x>v] \to p_x\)\([a_x\le v] \to \min(0,p_x)\),我们可以把它写成一个很神奇的式子,\(\min(0,p_x)+[a_x>v]\max(p_x,0)\),简单分类讨论发现它是对的。我们现在只需要考虑如何维护这个式子,我们先把 \(w(l,r,v)\) 差分开,记 \(w(l,r,v)=s_{v,r}-s_{v,l-1}\),则 \(s_{v,x}=\sum_{i=1}^x \min(p_i,0) +\sum_{i \le x,a_i>v} \max(p_i,0)\)(和 \(f\) 一样,实际实现并不需要 \(v\) 因为 \(v=b_{j-1}\),每一轮的时候就相当于有这个的答案)。

我们考虑如何维护 \(s\),对于前半部分,是简单的前缀和,考虑后半部分的东西如何求得,这是一个二维偏序,我们可以使用 BIT 维护,具体的,在值域上做 BIT,对于若干次 \(s\) 的查询,我们把它们离线下来,按照 \(x\) 排序,扫的时候顺手维护,\(i\le x\) 是天然满足的,每次在 BIT 上查即可。

那么你考虑原式可以变成

\[f_{t,j}=\min\{f_{k,j-1}+s_{b_{j-1},t-1}-s_{b_{j-1},k}\} \]

枚举 \(k\) 的话,你发现最坏会被卡到 \(O(n^2)\),经典优化,提出常数项:

\[f_{t,j}=s_{b_{j-1},t-1}+\min\{f_{k,j-1}-s_{b_{j-1},k}\} \]

最值优化即可。时间复杂度 \(O(n \log n)\)

还有一些边界情况需要考虑:

  • 对于 \(b_1=a_t\) 的情况,由于没被删的第一个元素总是满足,所以前面必须都删掉。
  • 对于 \(a_t=b_m\) 的情况,后面将不能再出现任何前缀最大值,这其实就是我们定义的 \(s\),即最终答案为 \(\min_{a_t=b_m} \{f_{t}+s_{b_m,n}-s_{b_m,t}\}\)

实际实现很麻烦。。。

upd::2026.8.28.17:42,发现了一种可以去掉离线二维偏序的做法,洛谷题解好像没有同做法?你考虑 \(b\) 递增,所以天然就是离线的,一开始把所有点都加入 BIT,随着 DP 的过程,把不满足的数删除即可,同时注意这里不用值域上的 BIT,而是以下标去做,每次查询下标 \(\le x\) 的即可,弱爆了。。

CF2112E Tree Colorings

link

我是蠢蛋。。。好像只会正向,做了一会就去看题解了。首先我们考虑转化我的合法性条件,你发现只要一个地方接上了蓝,它的子树内必须全是蓝,黄也是一样的。对于绿,接蓝的或者绿的都可以,那么我们就会了一个正向的 DP,\(f_{u}\) 表示 \(u\) 子树内的方案数,对于它的每个儿子 \(v\),绿色有 \(f_v\) 中,蓝色黄色各有一种,各个子树独立,所以方案数是 \((f_v+2)\) 的乘积,然后就不会了。。。

那么考虑正解吧,你定义 \(g_{i}\) 表示有 \(i\) 种方案的树最少有多少个点,于是考虑我们单独拉出来一个儿子的子树,记它的方案数为 \(x+2\),于是 \(i=(x+2)y\)\(y\) 表示我们剩下的方案数,也就是整棵扣掉 \(x+2\) 这个子树以后的树,点数就是两个东西的和,那么转移考虑出来了,就是 \(g_i=\min_{(x+2)|i}\{g_{\frac{i}{x+2}}+g_{x+2}\}\),但是如果你对于每个 \(i\) 都去枚举因数的话,每次是根号的,那么我们用一下好久之前小波教的一个 trick,考虑调和级数预处理出每个的因数,\(O(n \log n)\),所以总的遍历也是 \(O(n\log n)\) 量级的,那么就做完了。

AT_agc040_e [AGC040E] Prefix Suffix Addition

link

啥叫我模拟赛遇到只会暴力 DP 然后神秘错误怒挂 \(40\)pts 啊。。。。

下文称 \(A_i=a_i\)

先说一下赛时想法,首先考虑一个点的最终值可以为拆分成前缀和后缀的贡献,我们分别记为 \(x_i,y_i\),于是 \(x_i+y_i=a_i\),考虑一下最终的答案,你贪心的想一次能覆盖的肯定就要全部覆盖掉,因此总的操作数就是 \(\sum_{i=0}^{n} [x_i>x_{i+1}]+[y_i<y_{i+1}]\),可以解释一下为啥是 \(0\)~\(n\),原序列还是只有 \(1\)~\(n\) 的但是你考虑如果全上升还需要一次,这样处理比较方便。于是我们考虑一个简单的 DP,\(f_{i,j}\) 表示你考虑到第 \(i\) 位,\(x_i=j\) 的最小操作数,我们天然知道 \(y_i=a_i-x_i\),因此考虑枚举 \(x_{i-1}\),记为 \(k\),则 \(f_{i,j}=\min_{k}\{f_{i-1,k}+[k>j]+[a_{i-1}-k<a_i-j]\}\),直接转移的复杂度是 \(O(nV^2)\)。考场上止步于此了。。。

查看了题解,考虑固定 \(k\),于是随着 \(j\) 的增大 \([k>j]\) 只降不增,\([a_{i-1}-k<a_i-j]\) 也是只降不增,我的 \(k\) 已经固定,因此归纳得知 \(f_{i,j}\) 随着 \(j\) 的增长单调不增。

继续考虑,对于 \(f_{i,0}\)的上界其实不超过 \(f_{i,a_i}+2\),感性理解,只要取同一个 \(k\) 就好了,最多两种变量。具体证明的话,考虑存在一个 \(k\),我们令 \(f_{i,a_i}=f_{i,k}+[k>a_i]+[a_{i-1}-k<a_i-a_i]\),显然可以化简为 \(f_{i,a_i}=f_{i-1,k}+[k<a_i]\),此时你考虑拿同一个 \(k\) 去带到 \(f_{i,0}\) 上,\(f_{i,0}=f_{i-1,k}+[k>0]+[a_{i-1}-k<a_i]\),前半部分相同,后半部分最多差 \(3\),因此 \(f_{i,j}\) 最多只有三种取值。

考虑转移时我都要考虑哪些 \(k\)\(f_{i-1,k}\) 必然有也是形如三个相同值的段,我们只要考虑每个段的左边界就可以了,二分即可,那么就会了 \(O(nV\log V)\)

考虑进一步压缩状态,你发现我根本不需要维护全部的状态,这是一个单调递增的函数,可以直接二分得到三个关键值的位置,找到三个位置分别转移即可,时间复杂度 \(O(n\log V)\) 可以通过。

P3267 [JLOI2016/SHOI2016] 侦察守卫

link

困难题。严肃认识到自己的树形 DP 能力不如一只区。

首先你会发现一个关键的性质:一个子树 \(u\) 如果能向上覆盖 \(i\),那么它必然也可以向下覆盖距离 \(i\) 以内的点。感性理解,因为是 \(u\) 子树内的点所以向上覆盖的距离必然不小于向下覆盖的距离。

考虑 \(u\) 这棵子树的答案。树外如果想影响 \(u\) 的子树,路径一定经过 \(u\);而 \(u\) 子树里面的守卫如果想影响外面,路径也一定经过 \(u\)。然后考虑在一棵子树内我们究竟关心什么:一个是最远的未覆盖点能不能被覆盖到,另一个是我子树里的守卫能不能帮一下外面。所以我们先想到一个暴力 DP:对于每个状态,记录 \(u\) 子树中最远未覆盖关键点距离为 \(i\),以及 \(u\) 最近的守卫距离为 \(j\)。这个状态还是比较自然的。然后考虑怎么砍掉状态。我们先进一步刻画 \(i,j\) 的含义:\(i\) 表示我还需要外面帮助我多少,\(j\) 表示我还能帮助外面多少。再结合一开始得出的性质:一个子树 \(u\) 如果能向上覆盖 \(i\),那么它必然也可以向下覆盖距离 \(i\) 以内的点。

于是分类讨论:

  • 如果子树内还有点没有被覆盖,那么未来必须有外界守卫进来救,而这个未来的外界守卫一定会支配掉当前内部守卫的对外贡献,因此不需要记录最近守卫,只需要记录深层的覆盖情况。因此定义 \(g_{u,i}\) 表示在 \(u\) 子树内,满足 \(dis(u,x)\ge i\) 的关键点都被覆盖的最小花费。
  • 如果当前子树已经全部被覆盖,那么就只需要考虑当前对外的贡献,所以记录我还能向外最多帮助多少。定义 \(f_{u,i}\) 表示完全覆盖了 \(u\) 子树,并且向上还能够覆盖距离 \(i\) 的最小花费。

考虑转移,我们去刻画 \(u\) 子树内儿子合并的过程。也就是前面已经加入了一些子树,现在再加入一个新的儿子 \(v\)

有了一开始的性质,我们知道,去覆盖新加入的部分只有两种情况:

  • 用原来 \(f_{u,i}\) 里的守卫去主要覆盖。考虑 \(f_{u,i}\) 能够帮助覆盖的就是满足 \(dis(x,u)\le i\) 的点。对于新的 \(v\) 来说,因为 \(v\)\(u\) 又多了 \(1\) 的距离,所以只要自行处理满足 \(dis(x,v)\ge i\) 的点即可,也就是 \(g_{v,i}\)。于是有 \(f_{u,i}=\min(f_{u,i}+g_{v,i})\)

  • \(v\) 子树里的守卫去主要覆盖。你考虑要覆盖到 \(u\) 的第 \(i\) 级祖先的话,从 \(v\) 出发就要覆盖到 \(i+1\),因此需要 \(f_{v,i+1}\)。剩下的与第一种类似,只需要原来的部分满足 \(g_{u,i+1}\) 就好了,也就是 \(f_{u,i}=\min(g_{u,i+1}+f_{v,i+1})\)

所以最终:

\[f_{u,i}=\min(f_{u,i}+g_{v,i},g_{u,i+1}+f_{v,i+1}) \]

考虑 \(g\) 的更新。加入一个新节点 \(v\) 时,我希望对于 \(v\) 子树中的任意节点 \(x\),满足 \(dis(u,x)\ge i\)

因为 \(v\)\(u\) 的儿子,所以等价于 \(dis(v,x)\ge i-1\),因此还要加上前面已经确定的贡献:\(g_{u,i}=g_{u,i}+g_{v,i-1}\)

考虑还有一个细节:\(f\) 必然具有单调性,因为更远的地方能够覆盖到,那么更近的自然也能够覆盖到,所以要在转移完成后做 \(f_{u,i}=\min(f_{u,i},f_{u,i+1})\)\(g\) 也一样。当 \(i\) 增大时,满足限制、必须被覆盖的点变少了,因此状态限制更松,所以 \(g_{u,i}=\min(g_{u,i},g_{u,i-1})\)

最后来考虑初始化。发现我们只需要考虑一开始只有节点 \(u\) 自己时的状态,之后它会不断与儿子合并,先考虑 \(f\)

  • 如果 \(u\) 是关键点,那么 \(i=0\) 时也必须覆盖,所以 \(f_{u,0}=W_u\)
  • \(i\ge1\) 时,不管 \(u\) 是否是关键点,想要向上提供覆盖能力,都只能在 \(u\) 自己放守卫,因此 \(f_{u,i}=W_u\)
  • 如果 \(u\) 不是关键点,那么 \(f_{u,0}=0\)

然后再考虑 \(g\) 的初始化:

  • \(g_{u,0}\) 表示要求距离 \(\ge0\) 的关键点全部被覆盖,也就是整个当前子树全部覆盖,因此 \(g_{u,0}=f_{u,0}\)
  • \(i\ge1\) 时,此时子树内只有 \(u\) 一个节点,而 \(dis(u,u)=0<i\),所以没有必须被覆盖的点,因此 \(g_{u,i}=0\)

然后这个题就做完了。状态确实不是很容易想到,转移难度其实并不高,但是需要一点一点稳步地推导,显然我没有这种能力。

upd:2026.8.31 11:47,阅读了水星湖的题解,加深了一些对于状态定义的理解,以及对于自己想法的说明。

#include <bits/stdc++.h>
using namespace std;

const int N=5e5+5,K=22,INF=0x3f3f3f3f;

int n,D,w[N],f[N][K],g[N][K];
bool ok[N];
vector<int> e[N];

void dfs(int u,int fa){
    f[u][0]=ok[u]?w[u]:0;
    g[u][0]=f[u][0];

    for(int i=1;i<=D;i++)
        f[u][i]=w[u];

    f[u][D+1]=INF;

    for(int v:e[u]){
        if(v==fa) continue;

        dfs(v,u);

        for(int i=0;i<=D;i++)
            f[u][i]=min(
                f[u][i]+g[v][i],
                g[u][i+1]+f[v][i+1]
            );

        for(int i=D-1;i>=0;i--)
            f[u][i]=min(f[u][i],f[u][i+1]);

        g[u][0]=f[u][0];

        for(int i=1;i<=D;i++){
            g[u][i]+=g[v][i-1];
            g[u][i]=min(g[u][i],g[u][i-1]);
        }
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin>>n>>D;

    for(int i=1;i<=n;i++)
        cin>>w[i];

    int m;
    cin>>m;

    while(m--){
        int x;
        cin>>x;
        ok[x]=1;
    }

    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }

    dfs(1,0);

    cout<<f[1][0];
}

CF1476F Lanterns

link

额呃呃应该是好题但是我不会。每个东西都被覆盖,每个覆盖物都是连续的区间,可以考虑对于前缀的覆盖,将可达性刻画成最优性,我们定义 \(f_i\) 表示前 \(i\) 个灯,能覆盖的最长的前缀,考虑对于灯 \(i\),首先天然的肯定有 \(f_{i}=\max(f_{i},f_{i-1})\)。接着考虑灯 \(i\) 的覆盖,如果是向右覆盖,就必须满足 \(f_{i-1}\) 能够覆盖到 \(i\),即 \(f_{i-1} \ge i\),就可以 \(f_{i}=\max(f_{i},i+p_i)\)。如果是要向左覆盖,此时我们先二分到最小的 \(t\) 使得 \(f_t > i-p_i\) 即可,记 \(x_i=i+p_i\),然后让 \(f\)\([t+1,i-1]\)\(x_i\) 取最大值即可,构造就是记录一下转移路径。

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int p[N],f[N],st[N][20],pre[N];
char ans[N];
int query(int l,int r){
    if (l>r)return 0;
    int x=log2(r-l+1);
    return max(st[l][x],st[r-(1<<x)+1][x]);
}
int main(){
    int t;cin>>t;
    while (t--){
        int n;cin>>n;
        f[0]=f[1]=0;pre[1]=1;
        for (int i=1;i<=n;++i)cin>>p[i];
        for (int i=1;i<=n;++i)st[i][0]=p[i]+i;
        for (int j=1;j<20;++j){
            for (int i=1;i+(1<<j)-1<=n;++i){

                st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);

            }
        }
        for (int i=2;i<=n;++i){
            f[i]=f[i-1];pre[i]=i;
            if (!p[i])continue;
            if (i<=f[i-1]&&i+p[i]>f[i])f[i]=i+p[i],pre[i]=i;
            int t=lower_bound(f,f+i,i-p[i]-1)-f;
            if (t<i){
                int x=max(i-1,query(t+1,i-1));
                if (x>f[i])f[i]=x,pre[i]=t;
            }
        }
        if (f[n]>=n){
            cout<<"YES\n";
            int t=n;
            while (t){
                if (pre[t]==t)ans[t]='R',--t;
                else{
                    ans[t]='L';
                    fill(ans+pre[t]+1,ans+t,'R');
                    t=pre[t];
                }
            }
            ans[n+1]=0;cout<<ans+1<<'\n';
        }
        else cout<<"NO\n";
    }
}

基础算法

数学

posted @ 2026-08-24 16:10  Cefgskol  阅读(18)  评论(0)    收藏  举报