4月每日总结

4 月 1 日

我尽力,我无悔。

T1

对于那种只输入一两个数的题,首先不要去想正解,先猜半个小时结论看看猜不猜的出来。多半是猜的出来的

可是我写 T2 去了。

发现从 \(0\)\(\frac{1}{n}\) 的概率一发入魂。而剩下 \(\frac{n - 1}{n}\) 的概率就是在 \([1,n - 1]\)。对于每个点都有 \(\frac{1}{n}\) 的概率赢,有 \(\frac{n - 2}{n}\) 的概率留在 \([1,n - 1]\),那么对于每个 \(i \in [1,n - 1]\) 都是一样的。期望是 \(1 + \frac{(n - 1)^2}{n}\)

T2

赛时想法:设 \(dp_{i,j}\) 表示第 \(i\) 个点取值为 \(j\) 的方案数。显然有:\(dp_{i,j} = \sum\limits_{k = 1}^{a_i}dp_{i - 1,k} \times [k \neq j]\)。复杂度 \(O(nV^2)\),可以通过全局和优化到 \(O(nV)\),然后拿到 \(20\) 的好成绩。
我们发现这玩意可以简化成三个操作:区间取反,区间赋值,区间加。前面两个都是特化的区间乘。我们直接动开线段树,花了 2h 的时间获得的 \(60\)好成绩

正解是这样的:正难则反。我们想要求恰好\(0\) 个非法点的情况数,我们就尝试求钦定\(i\) 个非法点的情况数 \(F(i)\),然后反演一下得到最终的答案。

考虑如何求 \(F(i)\)。设 \(dp_{i,j}\) 表示前 \(i\) 个数分 \(j\) 段的方案数。显然有 \(F(i) = dp_{i,n - i}\)。有显然的转移:\(dp_{i,j} = \sum\limits_{k = 1}^{i}dp_{k - 1,j - 1} \times \min\limits_{l = k}^{i}a_l\)。复杂度 \(O(n^3)\),可以拿到 \(0\) 分的好成绩

我们发现我们不关心 \(j\) 的值,只关心它的奇偶性。我们就可以每次转移都乘一个 \(-1\),变成 \(dp_i = \sum\limits_{k = 1}^{i}-dp_{k - 1} \times \min\limits_{l = k}^{i}a_l\)。复杂度 \(O(n^2)\),可以获得 \(60\) 的好成绩。

最后用单调栈优化到 \(O(n)\) 即可通过。

T3

我们发现如果对于一个可行的方案,每个数都异或一个 \(x\),最终答案也可行。且总的异或和也异或了 \(y\)。所以我们不用考虑具体的方案,求出总方案数除以 \(2^k\) 就行。

考虑先填好第二行,方案数 \(2^k \times (2^k - 1) \times (2^k - 2) \times \cdots \times (2^k - n)\),然后钦定第一行有 \(i\) 个与第二行冲突,方案数为 \((2^k - i) \times (2^k - i - 1) \times (2^k - i - 2) \times \cdots \times (2^k - n + 1)\)。直接反演得到恰好有 \(0\) 个与之冲突的方案数。

后日谈

今天就是纯纯的数学题综合训练。于是开始预习复习概率与期望。

我尽力,我无悔。

4 月 2 日

不计代价。

不过我要是真的能做到 不计代价 的学习的话,我就不会那么菜了。

DFS Order 2

我们想到,一个子树内的 dfs 序肯定是连续的,我们考虑跑一个背包。设 \(dp_{u,i}\) 表示 \(u\) 在第 \(i\) 个被访问的方案数。我们先想总共有多少个 dfs 序。那么显然,如果有 \(tot_u\) 个子树,那么就有 \(tot_u !\) 个子树排列的方式。每个子树又有自己的排列方式,乘起来就是了。\(dp_{1,1}\) 就是总的排列方式数。初始化就完了。

考虑如何转移:我们想从 \(dp_{u,i} \to dp_{v,j}\)。我们发现,一次加的肯定是一整个子树。那么我们想:做背包!设 \(tmp_j\) 表示 两个点在 dfs 序上相距 \(j\) 的方案数。转移有显然的 \(dp_{v,j} \leftarrow dp_{u,i} + tmp_{j - i}\)

但是我们发现:子树之间是不分顺序的,我们转移 \(v\) 的时候还得扣掉 \(v\) 的子树。我们不可能对于每个子树都做个不考虑它的背包,那么复杂度直奔 \(O(n^4)\),显然不可取。

我们想:效仿淀粉质!每次扣掉 \(v\) 的子树所做的贡献,转移完再加回来。我们在每个 \(u\) 的子树转移时设单独的 \(f_{i,j}\)。表示在 \(i\) 个子树内总共选了 \(j\) 个点的方案数。那么初始化有 \(f_{0,0} = 1\)。转移有显然的 \(f_{i,j} \leftarrow f_{i - 1,j - siz_v}\)

那么我们又如何从 \(f \to tmp \to dp\) 呢?对于 \(f \to tmp\),有 \(tmp_{j + 1} = f_{i,j} \times i! \times (tot_u - i - 1)!\),三项分别表示 \(i\) 个子树内的方案数\(i\) 个儿子排列的方案数剩下儿子排列的方案数

但是我们输出发现:比例正确,值错误。我们直接化到最简。我们发现比例正确但是值不正确的原因是有 \(tmp_i\)所以就是直接化到最简

Core code:

\\ 预处理
int dfs1(int u,int fa)
{
    int res = 1;
    siz[u] = 1,tot[u] = 0;
    for (auto &v : g[u])
    {
        if (v == fa)
            continue;
        res = res * dfs1(v,u) % mod;
        siz[u] += siz[v],tot[u]++;
    }
    return res * fac[tot[u]] % mod;
}

\\ 转移
void dfs2(int u,int fa)
{
    memset(f,0,sizeof f);
    f[0][0] = 1;
    for (auto &v : g[u])
    {
        if (v == fa)
            continue;
        for (int i = tot[u]; i >= 1; i--)
            for (int j = siz[u]; j >= siz[v]; j--)
                f[i][j] = (f[i][j] + f[i - 1][j - siz[v]]) % mod;
    }
    for (auto &v : g[u])
    {
        if (v == fa)
            continue;
        for (int i = 1; i <= tot[u]; i++)
            for (int j = siz[v]; j <= siz[u]; j++)
                f[i][j] = (f[i][j] - f[i - 1][j - siz[v]] + mod) % mod;
        memset(tmp,0,sizeof tmp);
        for (int i = 0; i < tot[u]; i++)
            for (int j = 0; j <= siz[u]; j++)
                tmp[j + 1] = (tmp[j + 1] + fac[i] * fac[tot[u] - 1 - i] % mod * f[i][j] % mod) % mod;
        for (int i = 1; i <= n; i++)
            for (int j = 1; i + j <= n; j++)
                dp[v][i + j] = (dp[v][i + j] + dp[u][i] * tmp[j]) % mod;
        for (int i = tot[u]; i >= 1; i--)
            for (int j = siz[u]; j >= siz[v]; j--)
                f[i][j] = (f[i][j] + f[i - 1][j - siz[v]]) % mod;
    }
    for (auto v : g[u])
        if (v != fa)
            dfs2(v,u);
}

\\ 输出化简
for (int i = 1; i <= n; i++)
{
    int sum = 0;
    for (int j = 1; j <= n; j++)
        sum = (sum + dp[i][j]) % mod;
    int tmp = binpow(sum,mod - 2);
    for (int j = 1; j <= n; j++)
        printf("%lld%c",dp[i][j] * dp[1][1] % mod * tmp % mod," \n"[j == n]);
}

Great Expectations

原题也做不起吗?

我们考虑 ctj\(dp_{i,j}\) 表示在 \(i\) 个困难之后花费了 \(j\) 分钟的方案数。那么有转移:

\[dp_{i,j} = p_i \times dp_{i + 1,j + tim} + tim + \\ \begin{cases} \min\{dp_{0,0},dp_{i + 1,j + d_i + tim} + d_i + tim\} & j + d_i + n - t_i < r\\ dp_{0,0} & \text{otherwise.} \end{cases} \]

具体来讲,就是如果还有可能破纪录,就尝试能不能破纪录。反之直接重开。但是我们发现这玩意有后效性,直接高斯消元。高斯消元肯定是不可取的,毕竟有个 \(\min\) 在那里镇着。我们考虑二分 \(dp_{0,0}\)。如果最终的 \(dp_{0,0}\)\(mid\) 小,就说明有些本该重开的我们选择了继续打,往小二分。反之往大二分。

糖果大战

高斯消元 yyds

游走

此游走非彼游走。

设总共有 \(cnt\) 条路径,总长为 \(len\)。那么直接 dfs 搜出每个以每个点为起点的路径数量和路程总长,一加一除就出来了。

如果这题没模我不炸了?

汉堡 Burger

题意简化:有一个 01 串,0 的数量和 1 的数量各占一半。且最后的两个是连续的 1,求出这么滴的概率。

正难则反。设 \(ans_i\) 为长度为 \(i\) 的不成立的概率。那么显然有 \(\displaystyle ans_n = \frac{\binom{n - 2}{\frac{n}{2} - 1}}{2^{n - 2}}\)。我们发现这题没模,不能预处理阶乘。考虑推式子。推得 \(ans_i = ans_{i - 2} \times \frac{i - 1}{i}\)。答案就是 \(1 - ans_n\)

后日谈

我都写 Java 了我还管常数吗?

好想念小学时无忧无虑的周末啊。。。

Colourful Bottles

听 dpfs 讲的。

考虑 dp,设 \(dp_i\) 表示以 \(i\) 结尾,满足 \(k\) 连续的最小代价。考虑转移:

\[dp_i = \min\begin{cases} dp_{i - 1} + w_i & \text{直接删掉 } i \\ dp_{j - 1} + sumw_i - sumw_{j - 1} + sumc_i - sumc_j + w_j & \text{不删 } i \end{cases} \]

其中 \(sumw_i\) 表示 \(w\) 的前缀和。\(sumc_i\) 表示与 \(i\) 颜色相同的前缀和。第二个方程具体来说就是:删掉 \([j,i]\) 之间的所有数,但是同色的不能删,于是把它加回来。而我们不知道 \(j\) 的相同颜色的前驱是啥,于是直接减掉 \(sumc_j - w_j\)\(j\) 要满足 \([j,i]\) 之间至少有 \(k\) 个同颜色的。

考虑优化。对于每个颜色开一个 \(min\) 数组表示 \(dp_j - sumw_{j - 1} - sumc_j + w_j\) 的最小值。又对于每个颜色开一个队列。当队列长度等于 \(k - 1\) 时,就把对头取出,更新 \(min\) 数组。然后转移 \(dp_i = \min\{dp_{i - 1} + w_i,min_{c_i} + sumw_i + sumc_i\}\)。转移完了扔进队列。

后日谈

明天的模拟赛和重庆八中联考。又可以丢 CW 的脸了

每次一看到这个高达 \(\color{#ff0000}{16.7\%}\) 的得分率我就头大。明明打的对的题,总是因为一些莫名其妙的愿因挂分。要么是实现太优秀,常数爆炸,要么是没注意细节。

There's no time left.

4 月 8 日

今天是真的不颓。

T1

如果说有这样的 \(a = \{1,2\}\)\(b = \{100,101\}\),显然我们不能先加 \(a_1\),而应该先加 \(a_2\)。而后又看到 \(x\) 可以为负,想到如果说将 \(a_i\) 加上 \(x\) 使得 \(a_i = b_i\)\(a_i > a_{i + 1}\),我们就连一条 \(i + 1 \to i\) 的边。可以证明这是一个有向无环图。因此我们一定有用 \(\sum\limits_{i = 1}^{n}[a_i \ne b_i]\) 次的方法使得 \(a = b\)

现在我们考虑使代价更小。考虑对于一个 \((a_i - b_i)^2\),要使它变小,我们考率分多次。显然等分最优。赛时想到了做背包 dp,复杂度 \(O(nm^2)\)。可以拿到 \(\color{#5ec05e}{50}\) 的好成绩。看这个绿色就知道确实很好

正解是整一个大根堆。将每个 \((a_i - b_i)^2\) 多分一个的负贡献插入,比如初始就放每个 \((a_i - b_i)^2\)\(1\) 次到 \(2\) 次的负贡献。取 \(m - \sum\limits_{i = 1}^{n}[a_i \ne b_i]\) 次就好。

没放代码是因为下次再做的时候希望能自己写出来。

T2

听 HD0X 讲的。

看到冒泡排序,想到逆序对。离线,从大到小枚举 \(i\),并枚举每个 \(x = i\) 的查询,计 \(u\) 表示 \(i\) 位置前面的比它大的数量,\(v\) 表示 \(i\) 位置后面的比它大的数量。让我们分类讨论一下(下文的 \(k\) 都指查询的 \(k\)\(id\) 指查询编号,\(pos_i\)\(i\) 的原位置):

  • \(k \le u\)
    这样的话,\(i\) 前面的会在 \(k\) 次冒泡后跑到 \(i\) 后面去并把 \(i\) 往前挤,\(ans_{id} = pos_i - k\)
  • \(k \le u + v\)
    从头开始考虑 \(k\) 次冒泡。每次冒泡,一定会有一个比 \(i\) 更大的数 \(P_j\) 被一个更大的数 \(P_{j + 1}\) 堵住,且位置比 \(P_{j + 1}\) 左一个。
    我们掏个样例出来:\(\{4,3,5,1,2\}\)。我们想要查询 \(3\) 在第 \(2\) 次冒泡后的位置。
    \(1\) 次冒泡:\(\{3,4,1,2,5\}\)\(4\)\(5\) 堵住,位置 \(-1\)
    \(2\) 次冒泡:\(\{3,1,2,4,5\}\)\(3\)\(4\) 堵住,位置 \(-1\)
    这样我们看到 \(3\)\(1\) 的位置。这个 \(1\) 是由 \(5\) 最初的位置经过 \(2\)\(-1\) 后得到的。
    我们再掏一个样例:\(\{3,4,2,5,1\}\)。我们想要查询 \(2\) 在第 \(3\) 次冒泡后的位置。
    \(1\) 次冒泡:\(\{3,2,4,1,5\}\)\(4\)\(5\) 堵住,位置 \(-1\)
    \(2\) 次冒泡:\(\{2,3,1,4,5\}\)\(3\)\(4\) 堵住,位置 \(-1\)
    \(3\) 次冒泡:\(\{2,1,3,4,5\}\)\(2\)\(3\) 堵住,位置 \(-1\)
    \(2\)\(1\) 的位置。这个 \(1\) 是由 \(5\) 最初的位置经过 \(3\)\(-1\) 得到的。
    我们发现,因为在这 \(k\) 次里会被堵 \(k\) 次,所以最初的位置(就是上文的 \(5\) 的最初的位置)一定是比 \(i\) 大的数按照原序列的顺序的第 \(k\) 个。而每次被堵会使得下标 \(-1\),所以答案就是比 \(i\) 大的数的第 \(k\) 个的位置减 \(k\)
  • \(k > u + v\)
    显然这个时候 \(i\) 归位了,\(ans_{id} = n - u - v\)

实现可以这样:因为我们从大到小枚举 \(i\),用线段树维护。每次算完 \(i\) 都将线段树 \(pos_i\) 的地方置为 \(1\)\(u\) 就是 \([1,pos_i]\) 的区间和,\(v\) 就是 \([pos_i,n]\) 的区间和。“比 \(i\) 大的数按照原序列的顺序的第 \(k\) 个”可以通过线段树上二分找到第 \(k\)\(i\)。因为只有 \(k > u\) 时才会询问第 \(k\) 个,所以第 \(k\) 个一定在 \(i\) 原位置的右边。

T3 & T4

补题速度还是太快了一点,没看。

后日谈

之前的考试挂分 \(\color{#ff0000}{50}\) 起步,今天只挂了 \(\color{#5ec05e}{10}\) 分,可喜可贺。

要是 T1 没有被背包误导的话就更好了。好歹有 50 分嘛

今天晚上有 div.3,打不打呢?

4 月 9 日

弄懂每一题。然而我太蠢了

Square Subsequences

谁能给我讲讲 bitset 优化 LCS 啊?

简而言之就是枚举中间的分界点,然后对于两边的子串跑 bitset 优化 LCS。记录最大的 LCS 长度与其对应的 bitset。然后通过 bitset 反推 dp 数组,从而反推方案。时间复杂度 \(O(\frac{n^3}{\omega} + n^2)\),时限 \(2\) s,在 CF 神机上跑了 \(1.5\) s,换别的 OJ 还不一定跑的完。

然而我太蠢了没弄懂 bitset 优化 LCS。

Adjacent Delete

自从学了 C# 和 Java 以来,代码风格愈发面向对象了。与之而来的还有更大的常数。希望不要搞出在赛场上因为常数太大而 TLE 的事。

考虑没有相邻的限制如何做。显然是最大的 \(\frac{n}{2}\) 个减去最小的 \(\frac{n}{2}\) 个。现在考虑相邻的限制,这个上界也是可行的。我们把每个最大的 \(\frac{n}{2}\) 的标成 +,最小的那几个标成 -。显然无论如何都会有一对 +- 相邻。

对于偶数 \(n\) 显然就是那个上界。对于奇数 \(n\),因为会留下一个,留下的这个会将 \(a\) 数组分成两半,而两边都会取完,所以两边的长度肯定都是偶数。所以留下的只能是 \(a_1,a_3,a_5,\dots\),对于两边,就是上文的上界。我们可以用两个 multiset 维护。一个小根的维护大的那几个数,大根的维护小的那那几个数。每次如果插入的 \(x\) 大于等于小根堆的堆顶,就插入小根堆。否则插入大根堆。并维护一个堆内和。

放下我的常数超大代码:

#include<bits/extc++.h>
#define int long long
using namespace std;
const int maxn = 3e5 + 5;
int n;
int a[maxn];
class two_set
{
    private:
    multiset<int,less<>> mx;// 小根堆维护最大的几个数
    multiset<int,greater<>> mi;// 大根堆维护最小的几个数。
    void balance()
    {
        if (mx.size() > mi.size() + 1)
        {
            _min += *mx.begin();
            _max -= *mx.begin();
            mi.insert(*mx.begin());
            mx.erase(mx.begin());
        }
        if (mi.size() > mx.size())
        {
            _max += *mi.begin();
            _min -= *mi.begin();
            mx.insert(*mi.begin());
            mi.erase(mi.begin());
        }
    }
    public:
    int _min,_max;// 分别表示最小的数的和,最大的数的和。
    void insert(int x)
    {
        if (mx.empty() || x >= *mx.begin())
        {
            _max += x;
            mx.insert(x);
        }
        else
        {
            _min += x;
            mi.insert(x);
        }
        balance();
    }
    void erase(int x)
    {
        auto it = mx.find(x);
        if (it != mx.end())
        {
            _max -= x;
            mx.erase(it);
        }
        else
        {
            _min -= x;
            it = mi.find(x);
            assert(it != mi.end());
            mi.erase(it);
        }
        balance();
    }
}s1,s2;
void solve1()// 起码这里挺简洁的
{
    for (int i = 1; i <= n; i++)
        s2.insert(a[i]);
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        s2.erase(a[i]);
        if (i & 1)
            ans = max(ans,s1._max - s1._min + s2._max - s2._min);
        s1.insert(a[i]);
    }
    printf("%lld",ans);
}
void solve2()
{
    sort(a + 1,a + n + 1);
    int _max = 0,_min = 0;
    for (int i = 1; i <= n >> 1; i++)
        _min += a[i];
    for (int i = (n >> 1) + 1; i <= n; i++)
        _max += a[i];
    printf("%lld",_max - _min);
}
signed main()
{
    scanf("%lld",&n);
    for (int i = 1; i <= n; i++)
        scanf("%lld",a + i);
    if (n & 1)
        solve1();
    else
        solve2();
    return 0;
}

后日谈

yzp 因为鞋子没垫鞋垫导致他极其不舒服。然后今天也是十分颓废,只做了 \(3\) 题。

yzp 说他想回家,我的评价是:住校生是这样的。

不过住校生不开心还可以逃回家,走读生逃回家也没用,因为天天回家,对家已经无感了。不开心的时候只能畅想去到异世界。中二病

4 月 11 日

沟槽的树套树,沟槽的体考。

Two Permutations

我们发现,\(P_i\)\(Q_i\) 是绑定的,不会分开。如果 \(P,Q\) 都无序十分的难做且 \(P,Q\) 的顺序是不变的。我们可以将 \(P\) 排序,然后考虑 \(Q\)

我们建一个模型:将 \(P,Q\) 和与之对应的 \(R\) 看作上下两排。如果有 \(u < v\),那么就建一条 \(u \to v\) 的边。边可以是横着(\(P,Q\) 内部建边),也可以是竖着 (\(P \to Q\)\(Q \to P\))。非法当且仅当出现了环。

我们考虑 \(Q\) 中的最大值 \(Q_u\)

  • 如果 \(u\) 是向上建边的起点,说明 \(S_u\)\(S\)\(u\) 开始的后缀都是 \(0\),往上建边。因为 \(P\) 已经按照从小到大排序了,后面的一定比前面的大。而 \(Q\) 这里又是最大值,后面一定比 \(Q_u\) 小,那么说明这段后缀都是向上建边。这有什么用?这就说明后面不会成环且 \(S\) 确定了,不用考虑了。
  • 如果 \(u\) 是向下建边的终点,只能说明 \(S_u = 1\)。但是因为 \(Q_u\) 是最大值,所以它的边一定是往左右的,不会成环。

所以我们可以把题意转化成这样:有一个序列 \(Q\),每轮可以执行一次两种操作:

  1. \(Q\) 的最大值 \(Q_u\) 及从 \(u\) 开始的后缀删掉。
  2. 只删掉 \(Q\) 的最大值 \(Q_u\)

求出删完 \(Q\) 的方案数。

结论:答案就是 \(Q\) 的上升可空子序列数量。如何理解?我们只考虑操作 1。上升子序列就是一种操作 1 的操作序列。因为每次取走的是一段后缀且 \(Q_u\) 是最大值,所以下一个操作的就只能是 \(u\) 前面的比 \(Q_u\) 小的数。

那有人要问了?操作 2 你没考虑啊?其实也是考虑的。因为是子序列,所以里面可能夹杂着有空位,这些空位就是操作 2 的地方。因为操作 2 的 \(S\) 只能是 \(1\),方案数是 \(1\),所以不用计算。

树状数组优化 dp 即可。

Guess Sequence

我们对于 \(S_i \to T_i\) 建一条边。那么整个图一定是一棵树。如果不是一棵树那么就一定有点没被连边,这个点就可以乱取。

我们考虑对于一个点 \(u\),如果 \(b_u\) 乘上了 \(x\),那么 \(b_v\)\(v\) 是与 \(u\) 相邻的点)为了乘积不变,就得乘上 \(\frac{1}{x}\)。经过一番推导,我们发现,点 \(v\) 的深度与 \(u\) 的奇偶性相同,\(b_u\) 乘上了 \(x\)\(b_v\) 也得乘上 \(x\),反之乘上 \(\frac{1}{x}\)。这样我们就把 \(n\) 个点分成了两个集合。深度为奇的成一个集合,深度为偶的成另一个集合。

我们发现,如果一个集合的数里有个共同的因数 \(x\),那么就可以把当前集合里的的乘上 \(\frac{1}{x}\),另一个集合里的就乘上 \(x\),可以构造一个新的 \(b\),显然不行。所以我们需要把 \(a\) 分成两个 \(\gcd\)\(1\) 的集合,如果可行就是 Yes,反之就是 No

后日谈

什么两米八三,我才跳了一米八三。

事情的起因是这样的:我们去体考,考立定跳远。前面的贾皓博第一跳只跳了 \(2.4\) 米多。我就在想:是不是成外的测量器具有问题啊?我之前的 \(2.6\) 米是不是不真啊?于是第一跳就全力。大概是腿太粗?力量太大?

跑完 \(1000\) 米直接燃尽了,整个下午都没法学习。

4 月 13 日

一起把周六的日记也写了吧。

考试爆零。机房 rating \(16.7 \to 13.7\),决心 \(-\inf\)

T1

神秘小 dp,没改,明天改。

T2

想到了可持久化并查集,没想到和那个可持久化并查集一起的 Kruskal 重构树。

我这么菜不适合 Kruskal 这个英文名。

T3

主席书双 \(\log\) 做法还没调出来,单 \(\log\) 做法待会再说。

但是我拿测速代码测了 cwoi 的机子每秒大概 \(4.1 \times 10^8\),而 \(3 \times 10^5\) 的双 \(\log\) 也才 \(1.1 \times 10^8\),没准双 \(\log\) 卡卡常能过?

T4

神经分讨。

考虑 \([l,r]\) 区间内的最大值 \(a_x\) 出现在三个区间中的哪个位置。

  • 中间
    显然中间对于答案的贡献无论如何都是 \(a_x\),而两边的段随着长度的减小,贡献显然不会增加。那么最优方案就是 \(a_l + a_r + a_x\)
  • 左边
    显然左边的代价无论如何都是 \(a_x\)。根据上文的结论,第二个区间的长度越小贡献越小。所以整个段的答案就是 \(a_x + \min\limits_{r' = x + 1}^{r - 1}\{a_{r'} + \max\limits_{i = r' + 1}^{r}\{a_i \}\}\)。我们离线,从 \(1 \to n\) 枚举每个 \(r\) 及其对应的询问。设 \(f_i\) 表示当前 \(r\) 所对应的 \(\min\limits_{r' = x + 1}^{r - 1}\{a_{r'} + \max\limits_{i = r' + 1}^{r}\{a_i \}\}\)。用单调栈 + 线段树维护。具体而言是这样的:维护一个单调递减的单调栈。如果当前的 \(a_r\) 把栈顶 \(stk_{top}\) 弹出了,由于 \(a_{stk_{top}}\) 是之前的 \(r' \in [stk_{top - 1},stk_{top})\)\(\max\limits_{i = r' + 1}^{r}\{a_i \}\),现在被 \(a_r\) 替换了,所以在线段树上的 \([stk_{top - 1},stk_{top})\) 区间加 \(-a_{stk_{top}} + a_{r}\)。还要单独更新 \(r - 1\)。枚举每个 \(r_i\) 是当前枚举的 \(r\) 的询问, 那么答案就是 \(a_x + \min\limits_{i = x + 1}^{r - 1}\{f_i\}\),直接线段树区间查最小值就是了。
  • 右边
    把整个序列翻转,然后做左边一样的就行。

后日谈

感觉我一事无成。

4 月 14 日

想说点什么但是说不出来。

Transforming Pairs S

也是切上绿题了。

看到这玩意很像 \(\gcd\) 的求法,我们看看能不能往 \(\gcd\) 上面想。

口胡了一个做法:我们钦定 \(c < d\)。首先我们对 \(d\) 进行操作。将 \(d\) 减去 \(\lfloor \frac{d - b}{c} \rfloor \times c\)。这样可以保证 \(d \ge b\)。对 \(c\) 进行同样的操作,减去 \(\lfloor \frac{c - a}{d} \rfloor\),同样为了保证 \(c \ge a\)

继续口胡的证明:感性理解,我们为了让 \(c\)\(d\) 尽快的减成 \(a\)\(b\),每次减的肯定要尽可能多。于是我们每次进行辗转相减,减到再减一次就会爆炸的程度,每次减得就尽可能多了。

Moo Decomposition G

Java 的常数还是太优秀了。

我们发现 \(L \le 1 \times 10^{18}\),直接把整个序列拎出来做肯定是不现实的。但是我们发现一个性质:每段的方案数相互独立。那么我们就可以把每段的方案数单独求出来然后快速幂了。

考虑如何求每段的方案数:从后往前枚举每个字符,我们设 \(cnt\) 表示当前遇到的 \(\texttt{O}\) 的个数。

  • 遇到了个 \(\texttt{O}\)
    显然是 \(cnt \leftarrow cnt + 1\)
  • 遇到了个 \(\texttt{M}\)
    这个 \(\texttt{M}\) 可以从前面的 \(cnt\)\(\texttt{O}\) 里面任意取 \(k\) 个,方案数乘上 \(\binom{cnt}{k}\)。然后因为用掉了 \(k\)\(\texttt{O}\),我们需要把 \(cnt\) 减去 \(k\)

Bessie's Function G

一道基环树板子题硬控我一下午。

我也并非人类。

我们发现这玩意可以套一个典中典的模型:对于每个 \(i\),连一条 \(i \to a_i\) 的边。因为总共有 \(n\) 条边,所以一定是一个基环树森林。

我们发现如果要修改 \(a_i\),一定是要把 \(a_i\) 修改成 \(i\),因为这样不仅可以使得 \(i\) 变得满足条件,还能使出边连接到 \(i\) 的点满足条件。但是为了方便 dfs,不可能建内向的基环树,肯定是建外向的基环树,就变成了让 \(a_i = i\) 可以使得 \(i\) 的儿子们也满足条件,我们就可以把题意转化一下:

对于每个点 \(u\),都可以花费代价 \(c_u\) 进行一次操作。操作必须要满足以下条件:如果你不操作那你的所有前驱就必须操作(在树上“前驱”表示儿子,在环上表示上一个节点。),反之你的前驱爱咋咋地。求最小代价。

我们这个题意都把方程给你写出来了。设 \(dp_{u,0/1}\) 表示 \(u\) 操作或者不操作。在树上就是:

\[\begin{aligned} dp_{u,0} & = \sum_{v \in son_u}dp_{v,1} \\ dp_{u,1} & = \sum_{v \in son_u}\min\{dp_{v,0},dp_{v,1} \} \end{aligned} \]

在环上也类似。如何处理环上 dp 呢?我们考虑钦定 \(cir_0\) 的状态。比如我们钦定 \(cir_0\) 选,那么 \(dp_{cir_0,1}\) 就设成 \(\inf\),最后统计答案最后一个点 \(u\) 就爱选不选,\(tmp = \min\{dp_{u,0},dp_{u,1}\}\)。反之就是 \(dp_{cir_0,0} = \inf\)\(tmp = dp_{u,1}\)。最后 \(ans\) 加上两个 \(tmp\) 的最小值就是了。

以后有向图找环还是老老实实 dfs 吧。

Shorten the Array

都想到了 \(85\%\) 了为啥不再想一会呢?

#include<bits/extc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn = 2e5 + 5;
int n,k,cnt;
int ch[maxn * 30][2],pos[maxn * 30];
void ins(int x,int id)// 字典树插入
{
    int rt = 1;
    for (int i = 30; i >= 0; i--)
    {
        bool dig = (x >> i) & 1;
        if (!ch[rt][dig])
            ch[rt][dig] = ++cnt;
        rt = ch[rt][dig];
        pos[rt] = max(pos[rt],id);// 维护这个这个子树内的最大 id
    }
}
int que(int x)
{
    int sum = 0,res = 0,rt = 1;
    for (int i = 30; i >= 0; i--)
    {
        bool dig = (x >> i) & 1;
        if (ch[rt][dig ^ 1])
        {
            if ((sum ^ (1 << i)) >= k)
            {
                res = max(res,pos[ch[rt][dig ^ 1]]);
                rt = ch[rt][dig];
                // 如果可以 >= k 就记录答案,然后往另一个子树走,看看能不能更大。
            }
            else
            {
                // 否则肯定要异或。
                sum ^= (1 << i);
                rt = ch[rt][dig ^ 1];
            }
        }
        else if (ch[rt][dig])
            rt = ch[rt][dig];
        else
            break;
    }
    return res;
}
void solve()
{
    for (int i = 1; i <= cnt; i++)
        ch[i][0] = ch[i][1] = pos[i] = 0;
    cnt = 1;
    scanf("%d%d",&n,&k);
    int ans = inf;
    for (int i = 1,x; i <= n; i++)
    {
        scanf("%d",&x);
        ins(x,i);
        if (int tmp = que(x); tmp)
            ans = min(ans,i - tmp + 1);
    }
    if (k == 0)
        return (void)puts("1");
    if (ans == inf)
        puts("-1");
    else
        printf("%d\n",ans);
}
signed main()
{
    int t;
    scanf("%d",&t);
    while (t--)
        solve();
    return 0;
}

后日谈

今天也是终于把 Java 的传参搞懂了。

4 月 15 日

人不要太贪心。\(110\) pts 达成目标了就行了不要再想更高的分。

机房 rating \(13.7 \to 15.0\)

T1

考虑对于每个 scc 分别算贡献。因为只有 \(n\) 条边,所以只会形成基环森林,不会形成那些乱七八糟形态的环套环。

  • 如果这个 scc 里一个没取,那么对于答案的贡献就是 \(1 \times 2^{n - siz}\),因为有总共的一个 scc,而外面的怎么取都无所谓,方案数是 \(2^{n - siz}\)
  • 否则枚举这个 scc 里取了 \(j\) 个点,那么会形成 \(siz - j\) 个 scc,方案数是 \(\binom{siz}{j}\),外面是方案数还是 \(2^{n - siz}\),那么贡献就是 \((siz - j) \times \binom{siz}{j} \times 2^{n - siz}\)

因为每个 scc 的点数和是 \(n\),所以时间复杂度 \(O(n)\)

T2 & T3 & T4

只打了个 T4 的暴力。据说 T4 是线性 dp?

jmr 的话我也信?

后日谈

之前为了抵御我的颓废之心,我就一直在心里给自己说:你看你都菜成这样你还颓废?然而现在是不颓废了,但是这个想法似乎是被深渊侵蚀了一样,考好了依然有,导致考多少都不知足。这是个大问题,不仅让我晚上睡不着觉,重要的是脑子里的奖励机制废了。没有来自内心的动力,我就变成了傀儡。

4 月 16 日

你甘心停留在 C 组吗?

T2

我们发现 \(m \le 1 \times 10^6\),这个 \(q\) 我们直接不管他,我们预处理 \(ans_i\) 表示询问 \(i\) 时的答案,到时候直接 \(O(1)\) 查询就是了。

考虑如何求 \(ans_i\)。发现对于每个 \(i\),如果独立的求显然不好做。考虑如何从 \(ans_{i - 1}\) 递推。我们发现这是一个神经的倍增。而上面的那一半和下面的那一半跳的步数是一样的。考虑求上面的那一半然后乘 \(2\)

如何求上面那一半呢?考虑何时 \(p\) 会继续往下跳。发现,会造成影响的只有 \(a_1,a_3,a_5,\dots,a_{2^n - 1}\),因为只跳的到这些位置。显然只有 \(O(\log_2 n)\) 个位置。我们直接枚举当前跳到了 \(a_p\)。何时它会继续往下跳,并造成 \(2\) 的贡献?(这里的贡献按 \(2\) 算了就不用执行上文的乘 \(2\) 了)

发现当且仅当 \(a_p \le i\) 的时候会继续往下跳并造成贡献。\(a_p \in [1,i - 1]\) 的我们已经在 \(ans_{i - 1}\) 里算过了,所以我们只考虑 \(a_p = i\)。那么这个位置会造成的贡献就是 \(\binom{i - 1}{p - 1} \times \binom{m - i}{n - p} \times 2\)。具体来说,因为 \(a\) 是单调递增的,所以我们要把 \(i - 1\) 个数取 \(p - 1\) 个填进 \([1,p - 1]\) 这个范围内。后面同理。最后乘上一个 \(2\) 作为贡献,就可以加进 \(ans_i\) 了。

T3

我不想也不能。

这是一个无根树。考虑如果有根(也就是有一个固定的终点怎么做)。我们发现可以做 dp。对于每个节点 \(u\) 记录一个 \(a_u\),里面存两个数,分别是到达 \(u\) 的最快的选手和他所用的时间。转移是平凡的。

考虑没有一个固定的根怎么办。对于一个无根树,我们就可以做换根 dp。考虑如和 \(O(1)\) 换根。我们只用在 \(a_u\) 里多记录几个值。分别是:第一个到达的与他的时间,第二个到达的与他的时间,第一个从哪里来。如何换根呢?我们考虑从 \(u\) 转移 \(v\)\(v \in son_u\))。我们发现:如果最快的不来自 \(v\),我们就直接拿他来转移 \(v\),反之就拿次快的转移 \(v\)。大力分讨即可。统计答案就离线,然后换根换到 \(t\) 的时候把每个 \(s\) 对应的得分记录一下就行。

别忘了还原。

void dfs2(int u,int fa)
{
    for (auto [s,id] : que[u])
        ans[id] = buc[s];// 桶记录得分
    for (auto [v,w] : g[u])
    {
        if (v == fa)
            continue;
        Klee tu = a[u],tv = a[v];
        buc[a[u].fir.second]--,buc[a[v].fir.second]--;
        if (a[u].from == v)// 如果来自 v 就换成次快的
            a[u].fir = a[u].sec;
        pii tmp = a[u].fir;
        tmp.first += w;
        if (tmp < a[v].fir)// 转移
        {
            a[v].sec = a[v].fir;
            a[v].fir = tmp;
            a[v].from = u;
        }
        else if (tmp < a[v].sec)
            a[v].sec = tmp;
        buc[a[u].fir.second]++,buc[a[v].fir.second]++;
        dfs2(v,u);
        // 别忘了还原
        buc[a[u].fir.second]--,buc[a[v].fir.second]--;
        buc[tu.fir.second]++,buc[tv.fir.second]++;
        a[u] = tu,a[v] = tv;
    }
}

Election Queries G

题读错了 + 码力太强。

具体就是:结论:设 \(v_i\) 表示 \(i\) 所获得的总票数。那么有结论:\(x,y\) 能够当选,当且仅当 \(v_x + v_y \ge vmax\)\(vmax\) 表示 \(v\) 的最大值。用 map 维护 \(v\) 里出现的值。set 维护每个值出现的位置。因为 \(v\) 只有 \(O(\sqrt{n})\) 种取值,所以每次直接枚举每个出现次数不为 \(0\) 的每个值,然后双指针找出最大的 \(|x - y|\) 使得 \(v_x + v_y \ge vmax\)

The Best Subsequence G

\(n \le 1 \times 10^9\),对于反转操作直接动开线段树。

考虑如何查询 \([l,r]\) 区间内的最大字典序子序列。显然是找到一个最右的 \(pos\) 使得 \(cnt_{l,cnt} + r - cnt \ge k\)\(cnt_{l,cnt}\) 表示 \([l,pos]\) 内的 \(1\) 的个数。这么做的原因是在前面尽量的取到更多更长的 \(1\)。二分找到最右的 \(pos\),维护“将其解释为二进制数时的值”线段树上维护即可。

后日谈

明天继续。

A 掉 T1 就是。但是也别因为没切掉 T1 就摆烂。无论怎样都把暴力打完。

4 月 17 日

没挂分,大大的

T1

还是没太懂?

T2

我不管,我的时间复杂度是正确的。

得分:\(70\) pts

T3

也是有人改。

高斯消元?反正赛时推式子推了一半推不动了。

得分:\(40\) pts

T4

爆搜获得了 \(20\) pts

tot:\(110\) pts

\(100\) 了,大大的赢!

后日谈

今天晚上回家写日记,究其原因是晚上听 yzp 讲 門松,讲的实在是清楚,导致最后一节晚自习没写日记。

明天去问 yza 吧。

4 月 18 日

今天才切了 \(4\) 道题,太颓废了。

門松

总之就是非常神奇。

昨天晚上是因为读错题了导致被硬控一晚上。今天早上是因为码力太强又被硬控一上午。

考虑对于每个偶数 \(i\),那么我们发现:\(\{a_{i - 1},a_i,a_{i + 1} \}\) 形成的三元组状态一定是 \(\{0,1\}\)。对于最终的四个状态,大力分讨即可。

这种大力分讨的题往往都很考验码力。但是我的码力又不行。

Ski Slope S

开启今天的主线。

考虑把询问离线下来。然后把它和边混到一个数组里。用结构体记录三个值:\(\{val,id,type\}\)。其中,\(val\) 表示这条边的难度/这头牛的水平。\(id\) 表示边的终点/询问的 \(id\)\(type\) 就表示类型。对于每个点 \(u\) 记录一个 \(dis_u\) 表示它到 \(1\) 的距离,\(cnt_u\)\(1\) 有多少条边大于当前的水平。我们从大到小枚举每一条边/每一个查询。

  • 如果这是一条边,那么就说明这条边的终点以及它的子树 \(cnt\) 都要加 \(+1\),因为 \(c_i \le 10\),我们用 \(10\)set 维护每个 \(cnt_u = [1,10]\) 的每个点。每次直接暴力 dfs 更新。剪枝:如果点 \(cnt_u > 10\),那么说明 \(u\) 和它的子树无论如何都不可能是答案,直接 return
  • 如果这是一个查询,那么我们就枚举 \(i = [0,c_{id}]\) 的每个值,在 \(set_i\) 里查找 \(dis\) 的最大值所对应的点。

记得把 set 的比较器重载成 \(dis_x < dis_y\)。还有,当有一些边的 \(val\) 和查询的 \(val\) 一样,那么点一定比边优先。

Sequence Construction S

考虑 \(k\) 在二进制下的第 \(i\) 位。如果其为 \(1\),就说明有一个数 \(x\)\(\operatorname{popcount}(x)\)\(2^i\),说明 \(x\)\(2^{2^i} - 1\)其实也不一定,但是我们按照这个策略构造是对的。

我们枚举 \(k\) 的每一个为 \(1\) 的位。将其记录进答案。并将其从 \(m\) 中减去。如果出现 \(m < 0\),就说明无解,因为我们已经按照最小方案来构造了。

对于 \(m\) 剩下的,如果它为偶数,就在答案序列的末尾添加两个 \(\frac{m}{2}\),这样既能保证和为 \(m\),又能保证异或和不受影响。如果它为奇数,就先加 \(\{1,2\}\),再加 \(\frac{m - 3}{2}\),原因同上。

记得特判 \(m\) 剩下的是 \(1\)

OohMoo Milk G

显然有 John 和 Nhoj 都会去操作最大的那几个数,于是我们把题意转化为:John 先对于最大的 \(A\) 个数加上 \(D\),然后,Nhoj 去减每个数。但是总次数不能超过 \(B \times D\),单个数减的次数不能超过 \(D\)

考虑二分。设当前二分到了 \(mid\)chk 时我们把尝试每个 \(i \in [1,A]\)\(a_i\),减到 \(mid\),但是因为有“单个数减的次数不能超过 \(D\)”的限制,所以有些数减不到 \(mid\)。如果总次数大于 \(B \times D\),那么就说明 \(mid\) 小了,往大二分。反之如果有剩余的,就把每个还能减的减到 \(mid - 1\),并计算答案,然后继续往小二分,尝试更大的答案。

后日谈

我要变强!

4 月 20 日

迟早要完

T3

将柿子转化一下,变成 \(\sum|T_i|\times |C_i - f|\),发现这玩意相当于:对于每个 \(i\),有 \(T_i|\)\(C_i\)。求所有数的中位数。直接上离线将 \(C_i\) 离散化。然后权值线段树。具体实现:树上节点存两个信息:\(sumt,sumc\),分别表示该区间的 \(T\) 之和和 \(C\) 之和。中位数直接查询 \(\lceil\frac{tot}{2}\rceil\) 的数就行。设中位数为 \(med\)。那么小于中位数的贡献是 \(med - C\),大于的是 \(C - med\)。我们可以查询 \(i \in [1,med]\)\(C_i\) 之和和 \(i \in (med,n]\)\(C_i\) 之和。这样就能做到 \(O(n\log n)\) 的复杂度。

后日谈

但是我们赛时都没做出来的题,deepseek 想了 \(8\) min 就过了?

吃枣药丸。

4 月 21 日

十分颓废。

Hot Potato

看到:\(n \le 20\),直接状压。一开始设的状态是:\(dp_{s,i}\) 表示已经走过的点的状态是 \(s\)\(i\) 的失败概率。但是我们发现:有了 \(s\),已经可以确定 \(i\) 是否失败了,而且这个状态转移也很麻烦。考虑换一种状态。

我们设 \(dp_{s,i}\) 表示走过的点状态为 \(s\),当前在 \(i\) 的概率。考虑如何转移。我们设当前 \(i\)\(cnt\) 个备选。循环每个与 \(i\) 有边的点 \(j\),那么显然转移有:\(dp_{s \cup \{j\},j} \leftarrow dp_{s,i} \times \frac{1}{cnt}\)。如果 \(cnt = 0\),就把 \(ans_i\) 加上 \(dp_{s,i}\),表示这种状态会输,并不进行转移。

时间复杂度 \(O(n^2 2^n)\),实现不能太劣。

Crashing Competition Computer

考虑设 \(f_i\) 表示不保存打 \(i\) 个字符的期望时间。那么显然有 \(f_i = f_{i - 1} + 1 + p \times f_i\)。转换一下就是 \(f_i = \frac{f_{i - 1} + 1 + p \times r}{1 - p}\)

\(dp_i\) 表示保存打 \(i\) 个字符的期望时间。初始化就是 \(dp_i = f_i\)。转移有 \(dp_i = \min_{j = 0}^{i}\{dp_j + t + f_{i - j}\}\)。表示在 \(j\) 的位置保存并打 \(i - j\) 个字符。

最后答案就是 \(dp_n + t\)

Dice Grid

唐题。

直接从 \([n,n]\) 开始 bfs 看看能不能给正方体的每个面都染色就行了。

后日谈

今天晚上有久违的 div.2。切掉 T3 就是半赢。切掉 T4 就是

4 月 22 日

大输特输。

Median Splits

如果有 \(a_i \le k\),就把 \(a_i\) 置为 \(1\),反之置为 \(-1\)。这样,如果一段区间 \([l,r]\) 的中位数 \(\le k\) 的充要条件就是 \(\sum_{i = l}^{r}a_i \ge 0\)。如何使三段区间的中位数的中位数 \(\le k\) 呢?显然是有两个区间的中位数 \(\le k\) 就行。直接大力分讨哪两个区间的中位数 \(\le k\)

  • 左边的和中间的区间
    维护一个前缀和 \(sum_i\)。如果 \(sum_i \ge 0\),就看看有没有小于等于 \(sum_i\) 的非负数出现在 \(sum\) 里比 \(i\) 靠前的地方 \(j\)。如果有,那么 \([1,j],[j + 1,i],[i + 1,n]\) 就是一组解。直接上权值线段树维护。
  • 右边的和中间的区间
    reverse 一下 \(a\),然后同上。
  • 左边的和右边的区间
    在上面的两次计算中,记录一个 \(x,y\) 分别表示两次计算中 \(sum_i\) 最早非负的位置。如果 \(x + y < n\),就说明两个区间中间还能再夹一个区间,就是可行的。

submission

Local Construction

大力分讨 + 差分约束。

好像神明知道我的码力不行,天天给我发大力分讨题。

Soldier Game

考虑枚举最小值。设 \(dp_{l,r,i,j}\) 表示在区间 \([l,r]\) 中,\(l\) 是否和 \(l - 1\) 配对,\(r\) 是否和 \(r + 1\) 配对的最小的最大值。用线段树维护。转移就是 \(dp_{i,j} = \min\{\max(dp_{l,mid,i,k},dp_{mid + 1,r,k,j})\}\),表示在诸多 \(\max(dp_{l,mid,i,k},dp_{mid + 1,r,k,j})\) 里取最小值。用线段树维护。将每种配对方式的战力从小到大排序。每次记录完答案就把值小于当前值的地方置为 \(\inf\)

小 A 的卡牌游戏

考虑只有两种牌怎么做。把牌按照 \(b_i - a_i\) 排序,然后按照排完序的顺序,先取 \(B\) 个再取 \(A\) 个。想想多加一个怎么做。

我们设 \(dp_{i,j}\) 表示在前 \(i\) 个 3pick 里取了 \(j\) 个 C。转移是显然的。如果我们不选 c,就按照上面的贪心策略转移:如果 \(i - j \le B\),就取 B,反之取 A。

后日谈

今天中午午觉没睡好。下午听 AC 自动机烧烤过度,还去打了个球。晚上回机房直接获得 debuff:疲惫;效果:智力 \(- \inf\),理解能力 \(- \inf\),记忆力 \(- \inf\)。然后晚上硬生生的把 KMP 和 AC 自动机搞懂了。

AC 自动机学习笔记 is coming soon.

4 月 23 日

少年啊成为神话吧!

T1

愿天堂没有 27

T2

愿天堂没有码力题。

T3

想到根号分治。我也不知道怎么想到的

发现我们暴力也具有优越性。因为它的时间复杂度是 \(O(n)\) 的,与 \(m\) 无关。那么在 \(m\) 较大的时候,暴力就是很优的。我们可以把 \(m \ge \sqrt{n}\) 的时候跑暴力。因为至多只有 \(\sqrt{n}\) 个询问拿来跑暴力,所以暴力这块的时间复杂度就是 \(O(n \sqrt{n})\)

考虑 \(m < \sqrt{n}\) 如何做。考虑维护一个 \(f_{i,j}\) 表示左端点 \(\in [1,x_i]\),右端点 \(\in [x_j,n]\) 的区间数。考虑如何用 \(f\) 计算 \(ans\)。我们可以用容斥原理。首先,考虑一个只覆盖 \([x_i,x_i]\) 的区间。那么只有 \(f_{i,i}\) 能计算它。\(ans\) 加上 \(f_{i,i}\)。继续考虑只覆盖 \([x_i,x_{i + 1}]\) 的区间。那么它会被 \(f_{i,i},f_{i + 1,i + 1}\) 计算它。但是它覆盖了两个点。所以它不能被计算,\(ans\) 加上 \(-2 f_{i,i + 1}\)。经过一番推导,我们发现容斥系数长这个样子:\(1,-2,2,-2,2,\dots\)。我们就可以计算了。如何维护 \(f\)?把每个 \(f_{i,j}\) 看成一次询问。将每次询问离线,有 \(O(n)\) 个询问。用树状数组维护即可,预处理时间复杂度 \(O(n\log n)\),每次询问 \(O(m^2 \log n)\)。我们发现,这是一个二次函数形式的复杂度,要卡满 \(m\) 就得是最大值 \(\sqrt{n}\),但是这样就最多只有 \(\sqrt{n}\) 个询问这样,总的复杂度就是 \(O(n\log n \sqrt{n})\)

照理来说 \(O(n\sqrt{n})\) 都跑不过 \(5 \times 10^5\),但是这玩意带 \(\log\) 都跑得过,数据是不是有点水啊?

后日谈

砸场没砸过,我的自尊心和明日香的自尊心一样,碎了。

昨天晚上的 CF,我居然还没有掉下青,说明青的实力十分的不行,得往蓝冲了。

4 月 25 日

回答我。

e-Government

建出 \(fail\) 树,当我们在字典树上查询文本串时,当前点 \(rt\)\(0\)\(fail\) 树上的路径上的点都代表模式串的一次全新出现。那么这个问题就变成了一个单点修改,链上查询问题。把每次的单点更新影响扩大到整个子树,那么就变成了一个区间修改,单点查询的问题,直接上树状数组维护。

You Are Given Some Strings...

对于文本串和每个模式串,正着建一个 ACAM,把它们 reverse 一下再建一个 ACAM,然后记录文本串的每个前缀的后缀被模式串匹配了多少次,最后枚举分界点就行了。

后日谈

没啥可谈的。

4 月 26 日

不知不觉 4 月都要结束了呢。

要学会爱自己呀。

数数

神经数位 DP + AC 自动机。

数位 DP 经典套路:记录 \(pos\) 表示当前到了多少位,还有 \(lim\)\(zero\) 两个 bool 分别表示有没有顶到上限,有没有前导零。但是这还要记录一个 \(rt\) 表示在字典图上到了哪个点。对于每个点 \(u\) 记录一个 \(ed_u\) 表示 \(u\) 是不是一个模式串的结尾。如果当前要去到的根 \(ed\)\(1\),就不进行转移,否则就 \(rt\) 跳并进行转移。

需要注意的是:在 \(fail\) 树上,如果 \(ed_u\)\(1\),那么 \(u\) 的整个子树的 \(ed\) 也得是 \(1\),在 bfs 处理 \(fail\) 的时候顺便处理了就是。

后日谈

愿此行,终抵群星。

崩铁两周年的主题曲是真的好听啊。

4 月 29 日

昨天晚上打 CF 去了,没写日记。

CF1400 的题都做不对的 OIer 有什么用?

Wonderful Lightbulbs

我们有结论:设最初的灯泡在 \((x,y)\),那么第 \(x\) 行一定有奇数个灯泡在亮,它所在的对角线也一定有奇数个灯泡在亮。

Wonderful Teddy Bears

首先我们把字符映射一下:\(\texttt{B} \to 0,\texttt{P} \to 1\)。考虑 \(4\) 种操作:

  1. \(\{0,1,0\} \to \{0,0,1\}\)
  2. \(\{1,0,0\} \to \{0,0,1\}\)
  3. \(\{1,0,1\} \to \{0,1,1\}\)
  4. \(\{1,1,0\} \to \{0,1,1\}\)

让我们从逆序对的角度考虑:对于第 \(1,3\) 种操作,只会减少 \(1\) 个逆序对,对于第 \(2,4\) 个操作,会减少 \(2\) 个逆序对。显然我们应该贪心的先做 \(2,4\) 操作直到做不动为止。

我们看何时 \(2,4\) 操作做不动了。当且仅当 \(a\) 变成形如 \(\{0,0,0,\dots ,0,1,0,1,\dots,0,1,0,1,1,1,\dots,1\}\),也就是说,\(a\) 可以被分成一个全为 \(0\) 的前缀和一个全为 \(1\) 的后缀,和一个 \(0,1\) 交替出现的字串。

因为 \(0,1\) 交替出现,我们考虑记录 \(a\) 表示总的 \(0\) 的个数,\(b\) 表示在偶数位置上的 \(0\) 的个数。依然考虑这 \(4\) 种操作:\(2,4\) 操作不会改变 \(b\)\(1,3\) 操作会使 \(b\) 变化(增加或减少)\(1\)。考虑 \(b\) 的最终形态是什么。因为 \(0\) 会全聚在 \(a\) 的前面,所以 \(b\) 的目标是 \(\lfloor \frac{a}{2} \rfloor\)。于是我们可以这样:设总共有 \(x\) 逆序对,先做 \(|b - \lfloor \frac{a}{2} \rfloor|\)\(1,3\) 操作。\(x\) 减少 \(|b - \lfloor \frac{a}{2} \rfloor|\),然后再做 \(\frac{x}{2}\)\(2,4\) 操作。

Unpleasant Strings

看到 \(\sum|t_i| \le 1 \times 10^6\),看能不能搞出一种 \(O(\sum|t_i|)\) 的算法。

我们想到预处理 \(s\) 串上每个下标 \(i\)\(ans_i\),表示以 \(i\) 结尾的子序列最少还要加多少个字符才能不再成为悦耳字符串。

考虑如何预处理。\(i\) 从后往前枚举,记录一个 \(lst_i\) 表示字符 \(i\) 最后出现的地方。我们枚举每个字符。有显然的转移:\(ans_i = \min\{ans_{lst_i} + 1\}\),表示在这个点后面接每个字符的方案。\(lst\) 初始全为 \(n + 1\),且 \(ans_{n + 1} = 0\)。再记录一个 \(son_{i,j}\),每次都把 \(lst\)memcpy\(son_i\)。转移完了再更新 \(lst\)。最后把 \(lst\) memcpy\(son_0\)

考虑如何处理询问。就像字典树一样,我们记录一个 \(rt\) 表示当前跳到了 \(rt\) 这个点。枚举 \(t\) 里每个字符 \(t_i\),每次跳到 \(son_{rt,t_i}\)。如果说跳着跳着直接跳到 \(n + 1\) 了,就说明它本就不是悦耳字符串,\(ans\) 置为 \(0\)break。反之如果跳到了 \(t\) 的结尾了都没跳出去,就说明这是一个悦耳字符串,答案即为 \(ans_{rt}\)

code:

#include<bits/extc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 5;
int n,k,q;
char t[maxn],s[maxn];
int son[maxn][26],ans[maxn];
signed main()
{
    scanf("%d%d%s",&n,&k,t + 1);
    fill(son[0],son[0] + k,n + 1);// 这里直接把 son[0] 当 lst 用了
    fill(ans + 1,ans + n + 1,inf);// ans 初始化 inf
    for (int i = n; i >= 1; i--)
    {
        memcpy(son[i],son[0],sizeof(int) * k);// 把 lst memcpy 进 son[i]
        for (int j = 0; j < k; j++)
            ans[i] = min(ans[i],ans[son[0][j]] + 1);// 转移
        son[0][t[i] - 'a'] = i;// 更新 lst
    }
    scanf("%d",&q);
    while (q--)
    {
        scanf("%s",s + 1);
        int rt = 0,len = strlen(s + 1);
        for (int i = 1; i <= len && rt <= n; i++)
            rt = son[rt][s[i] - 'a'];// 每次跳,如果跳出去了就说明本就不是悦耳字符串。
        printf("%d\n",ans[rt]);
    }
    return 0;
}

Bermuda Triangle

式子推推推推到厌倦

后日谈

去开拓把,即使前方道路未卜。

posted @ 2025-04-01 21:19  伊埃斯  阅读(93)  评论(0)    收藏  举报