做题记录

Week 1

2025/03/02

C. Beautiful Sequence

题目三个条件。如果有 \(1\le a_{i}\le 3\) 的话,那么如果我们对于每个位置都要这样满足,中间位置就只能填 \(2\),因为左边和右边需要比它小和比它大的数。其次,左边只能填 \(1\),否则第二个数就只能填 \(3\) 或者根本不能填,这时候,右边再想找到一个比它大的数是不可能的,但是我们限长为 \(3\),所以不合法。容易发现右边也等价的只能填 \(3\)。于是做由尾挂点即可。

具体统计,我们针对每一个 \(3\) 统计与前面所有的 \(1\) 中间 \(2\) 的个数,对于每一个 \(2\) 的个数 \(k\),算 \(2^k-1\) 加入答案即可。

但是这样太慢了,考虑优化,于是我们考虑继承,如果新加入了一个 \(2\),那么之前所有的 \(1\) 和后面 \(3\) 对答案的预备贡献都要加一乘二再减一(当然,所有 \(1\) 最开始的预备贡献是 \(0\)),当然可以用线段树,但是我们发现 \((x+1)\times 2 - 1=2x+1\),并且由结合律(我们可以把所有当前 \(1\) 的预备贡献都加起来,一起做运算),所以我们可以直接算,因为我们除了 \(1\),没有预备贡献,所以我们需要记录 \(1\) 的个数。

这题我们观察了性质,手玩了样例,运用了挂点的统计方法,运用了结合律。

Code:

// Problem: C. Beautiful Sequence
// Contest: Codeforces - Educational Codeforces Round 174 (Rated for Div. 2)
// URL: https://codeforces.com/problemset/problem/2069/C
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int X = 998244353;
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int tt;
    cin >> tt;
    while (tt--) {
        int n;
        cin >> n;
        vector<int> a(n + 10);
        upp(i, 1, n) cin >> a[i];
        int last = 0, ans = 0, cnt = 0;
        upp(i, 1, n) {
            if (a[i] == 1) cnt++;
            if (a[i] == 2) (((last *= 2) %= X) += cnt) %= X;
            if (a[i] == 3) ans += last;
        }
        cout << (ans + X) % X << endl;
    }
    return 0;
}

2025/03/04

D. Skipping

Sol 1

考虑一定有一个到达最右边的点,使得它前面的未跳过的点的决策全部都是完成。

假设这个点是 \(k\),可以证明他前面的所有未作决策的点一定可以做决策,如果中途选择跳过点的 \(b_{i}\le k\),跳过点就一定不优。并且显然,总会有一种最优的方案是通过某一个点 \(k\) 而计算的。而一个点 \(k\) 的答案就是 \(\sum_{i=1}^{k} a_{i}-sum_{skip}\)

因此,我们枚举这个点,这时候 \(\sum_{i=1}^{k} a_{i}\) 是一个定值,因此希望求最大值,只需要求 \(sum_{skip}\) 的最小值即可。

题目中提供了两种改变所在点的方式,一种是完成任务,我们不会改变 \(sum_{skip}\) 的值,一种是跳跃到 \(b_{i}\)\(sum_{skip}\) 会增加 \(a_{i}\)。明显构成图,分别有 \((i,i-1,0),(i,b_{i},a_{i})\) 这两条边,为了求最小值,跑最短路即可。

因此我们先依照上方方式建图,跑一边最短路,算出到达每一个点的 \(sum_{skip}\) 的最小值,枚举 \(i\),用 \(\sum_{i=1}^{k} a_{i}-sum_{skip}\) 更新答案的最大值即可。

时间复杂度 \(\mathcal O(m \log n)\)

Sol 2

考虑有以下观察,就是我们绝对不可能有包含的两次跳跃。

我们假设有 \(a,b,c,d\) 从左到右四个点。

有包含指的是,我们不可能既在决策中有在 \(b\)\(c\) 之间的跳跃,也有 \(d\)\(a\) 的跳跃。

有了这个观察,我们就有跳跃不包含的限制。

因此,我们可以认为在跳跃过程中,终点是递增的。

这给予了我们做 \(dp\) 的思路,因为我们看起来好像有了转移顺序。

这里我们用一下前面做法的结论,必然有点 \(k\) 作为 \(\sum_{i=1}^{k} a_{i}-sum_{skip}\) 可以取到整体答案。

所以我们用 \(f_{i}\) 表示最后一次跳跃是到 \(i\),中途 \(sum_{skip}\) 的最小值。

有状态转移方程:

\[\begin{aligned} f_{b_{i}}=f_{i}+a_{i} \end{aligned} \]

其中 \(f_{b_{i}}\) 应取最小值。

我们可以建一个 vector 数组来存储 \(b_{i}=k\) 的所有 \(i\),然后再内部排序。

然后我们再建一个 vector 数组来存储 \(f_{i}+a_{i}\) 前缀最小值即可。

每次转移一个 \(f_{i}\) 在 vector \(f_{i}\) 中二分最后一个小于 \(i\) 的决策点即可。

计算出 \(f_{i}\) 之后,我们就在 vector \(b_{i}\) 中二分等于 \(i\) 的决策点,然后更新前缀最大值。

最后我们枚举 \(i\),把答案与 \(\sum_{i=1}^{k} a_{i}-f_{i}\) 取最大值就行了。

好了,这就是这题的两种做法。都很有意思!期待我们都在 CF div 2 中解决 D 问题!

2025/03/05

E. C+K+S

首先,最开始给的图是合法的。

其次,由 tarjan 强连通算法的引理,我们知道,若这张图强连通,这张图的所有点就一定构成一个环。

也就是说针对原图染色对于所有 \((u,v)\) 边,都有 \(c_{u} \equiv c_{v}+1\pmod k\) 成立。

针对第一个点的染色,每个图,我们有 \(k\) 中不同的染色方案。

当我们想在图上连上一条边的时候,就需要让连上的边也满足这个性质。

我们可以证明当存在解时,一定有一种给原图染色的方法,使得添加的每条边都有这个性质。

于是,我们哈希的判断不同的染色方案时,是不是针对每一个图 1 中颜色为 \(c\) 的出点,都有图 2 中颜色为 \((c+1) \bmod k\) 的入点与之对应,以及是不是针对每一个图 2 中颜色为 \(c\) 的出点,都有图 1 中颜色为 \((c+1) \bmod k\) 的入点与之对应。也就是判断是否存在一种染色方案,使得图 1 中颜色为 \(c\) 的出点的数量和图 2 中颜色为 \((c+1) \bmod k\) 的入点的数量一样(以下称“条件 1”)以及图 2 中颜色为 \(c\) 的出点的数量和图 1 中颜色为 \((c+1) \bmod k\) 的入点的数量一样(以下称“条件 2”)这两个条件成立。

这样,我们考虑,对于原图 \((c_{i}+1) \bmod k\),这样可以构造一组全新的染色方案,并且这样其实相当于把原来的染色方案向右循环移位一位。

因此,对于一种染色方案,我们可以把每个图中的所有点按照 \(c_{i}\) 从小到大排序,我们把图 2 的点向左循环移位一遍,如果在每个位置上,图 1 图 2 的类型都不一样,我们就认为条件 1 成立。同时,我们根据同样的套路检查条件 2 是否合法。这里如果我们把每个图中点的类型反过来,就只需要检查两个序列是否一样即可(方法 1)。

这时候,时间复杂度是 \(O(n^3)\) 枚举两个图的染色方案,然后再验证。我们发现两边同时移位是无意义的,于是只枚举一个图的方案,时间复杂度来到了 \(O(n^2)\),接下来,考虑我们我们需要比较两个图的相等关系(方法 1),以及循环移位,所以考虑哈希字符串,我们就可以做到 \(O(1)\) 检查,\(O(n)\) 枚举。

这样,我们就解决了这题!

2024/03/06

C. Dora and C++

实际上,我们不仅可以加 \(a\),或者加 \(b\)

由于我们只在乎相对差,所以我们可以做减 \(a\),减 \(b\),加 \(\lvert a-b\rvert\) 等更多操作。

发现实际上,我们如果一直计算类似两个数相减的值,最终得到的一个数就会是 \(\gcd(a,b)\),这是由于辗转相除法。我们同样可以知道,所有数都是 \(\gcd(a,b)\) 的倍数。因此,考虑只对原数进行无限次加 \(\gcd(a,b)\) 的操作得到的答案和原问题的答案相等,也就是说,这两个问题等价。

接下来,求我们可以对每个数加或减 \(\gcd(a,b)\) 的答案。

两个数 \(x,y(x>y)\) 的差在模 \(\gcd(a,b)\) 意义下分别是 \((x-y) \bmod \gcd(a,b),\gcd(a,b)-(x-y) \bmod \gcd(a,b)\)

我们将所有 \(x \bmod \gcd(a,b)\) 从小到大排序作为 \(B\),然后最大值减最小值就是所有差全部取第一种的答案。

\(B_{n}-B_{1}\)

如果想取第二种,那么我们可以证明,只有寻找一个分割点,左边的全部加 \(\gcd(a,b)\),右边的全部不变才有可能取到答案。

\(\min\{B_{i}+\gcd(a,b)-B_{i+1}\}\)

两种情况继续取最小值。

// Problem: C. Dora and C++
// Contest: Codeforces - Codeforces Round 969 (Div. 2)
// URL: https://codeforces.com/contest/2007/problem/C
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f3f3f3f3f;
int ac[N], n, a, b;
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int tt;
    cin >> tt;
    while (tt--) {
        int ans = INF;
        cin >> n >> a >> b;
        int gcdd = __gcd(a, b);
        int maxn = -INF, minn = INF;
        upp(i, 1, n) {
            int x;
            cin >> x;
            ac[i] = x % gcdd;
        }
        sort(ac + 1, ac + 1 + n);
        ans = min(ans, ac[n] - ac[1]);
        upp(i, 1, n - 1) { ans = min(ans, ac[i] + gcdd - ac[i + 1]); }
        cout << ans << endl;
    }
    return 0;
}

D. Iris and Game on the Tree

有明显结论,值为 \(0\) 当且仅当经过的序列去掉无用的之后是一个回文串。

此时,只有 \(0,1\) 相间的部分才是有用的。

\(10010\) trans to \(1010\)

那么回文就变成了根节点和叶子节点的值一样。

于是可以得出既不是根节点也不是叶子节点的部分是完全无用的,因为它不会改变任何一个叶子的值。

如果根固定,两个人按照自己目标把叶节点未知的决定为与根节点相同或者不同就行了。

如果根不固定,并且叶节点两个值的数量不同,那么先手肯定选择将根决定成为与最多的叶子节点不同的值,这样容易证明绝对最优。然后按照上一问处理即可。

如果数量不同,情况就变成了,如果哪个人去决定根或者叶子的值,那么它就会变成第一种情况的后手,这显然不是很好。所以先手可以考虑去决定无关的点,这样如果无关的点是奇数个,先手就变成另一个人,并且他必须决定根或者叶子的任意一个从而变成后手。

好的,我们讨论完了,那么做法就显而易见了。代码很好写。

// Problem: D. Iris and Game on the Tree
// Contest: Codeforces - Codeforces Round 969 (Div. 2)
// URL: https://codeforces.com/contest/2007/problem/D
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int N = 2e5 + 10;
int h[N], ne[N], e[N], n, idx;
int color[N];
char a[N];
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}
void dfs(int u, int fa) {
    int flag = 1;
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (j == fa) continue;
        flag = 0;
        dfs(j, u);
    }
    if (u == 1)
        color[u] = 1;
    else if (flag) {
        color[u] = 3;
    } else
        color[u] = 2;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int tt;
    cin >> tt;
    memset(h, -1, sizeof h);
    while (tt--) {
        upp(i, 0, n) h[i] = -1;
        idx = 0;
        cin >> n;
        upp(i, 1, n - 1) {
            int x, y;
            cin >> x >> y;
            add(x, y);
            add(y, x);
        }
        upp(i, 1, n) cin >> a[i];
        dfs(1, -1);
        int sum1 = 0, sum2 = 0, cnt1 = 0; //叶子节点 无关节点
        int ans = 0;
        bool flag = 1;
        upp(i, 1, n) {
            if (color[i] == 3) ans++;
            if (a[i] == '?') {
                if (color[i] == 1)
                    flag = 0;
                else if (color[i] == 2)
                    sum2++;
                else
                    sum1++;
            } else if (color[i] == 3)
                if (a[i] != a[1] && a[1] != '?') cnt1++;
        }
        if (flag)
            cout << cnt1 + (sum1 + 1) / 2 << endl;
        else {
            int cnt2 = 0, cnt3 = 0; // 等于 1,等于 2
            upp(i, 1, n) if (color[i] == 3) cnt2 += (a[i] == '1'),
                cnt3 += (a[i] == '0');
            if (cnt2 == cnt3) {
                if (sum2 % 2)
                    cout << (cnt2 + (sum1 + 1) / 2) << endl;
                else
                    cout << (cnt2 + (sum1) / 2) << endl;
            } else
                cout << (max(cnt2, cnt3) + (sum1) / 2) << endl;
        }
    }
    return 0;
}

Week 2

C - Cost to Flip

发现,我们只有从 \(1\) 最终变成 \(1\) 是不确定的,我们可以先变成 \(0\),再变成 \(1\)。同样,我们也可以一直不变。

其他的操作只需以贪心顺序做的,我们把先变成 \(0\) 再变成 \(1\) 的操作看作是两个操作。这样我们先按照 \(c_{i}\) 从大到小做 \((1,0)\),再从小到大做 \((0,1)\)\((0,0),(1,1)\) 不用管。

所以先来决定 \((1,1)\) 到底哪些不变,哪些变。

有一个性质,假设我们有 \(m\)\((1,1)\) 那么,我们在所有选择 \(d(0\le d \le m)\),使得按 \(c_{i}\) 从小到大排序之后前 \(d\)\((1,1)\) 全部选择不变,后面全部变化,的决策之中一定包含最优决策之一。

可以使用反证法证明。

考虑式子计算麻烦,我们寻找 \(\Delta\) 即可,这个我们使用值域树状数组维护就行了。

posted @ 2025-03-02 20:11  PM_pro  阅读(16)  评论(0)    收藏  举报