CF2164

打得超级失败的一场。

CF2164B Even Modulo Pair

棒棒的观察题。注意到如果有两个偶数直接输出。如果只有一个偶数先把这一个 \(O(N)\) 做一遍。那么我们现在只考虑全都是奇数的情况。
必须注意到,如果 \(2 a_i> a_{i + 1} > a_i\) 那么此时一定有解。如果无解必须要满足 \(a_{i + 1} \ge 2a_i\)
因此,无解时序列长度最多 \(O(\log V)\),我们暴力用 \(O((\log V)^2)\) 就可判断出其无解。否则,我们至多用 \(O(n \log V)\) 的时间就可以找到一个解,因为前 \(O(\log V)\) 个数要么一定出现了相邻奇数有解的情况,要么一定在下一个出现了偶数。
代码写起来就是暴力。
取模题有很多类似于此的性质,比如这个。不会证的时候不妨先感受一些复杂度尝试写一下。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n;
void solve(){
    cin >> n;
    for(int i = 1; i <= n; ++i) cin >> a[i];
    for(int i = 1; i <= n; ++i){
        for(int j = i + 1; j <= n; ++j){
            if(!((a[j] % a[i]) & 1)) return cout << a[i] << ' ' << a[j] << '\n', void();
        }
    }
    cout << -1 << '\n';
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

CF2164C Dungeon

我们把题目分成两个阶段,一个是攻击 \(c_i > 0\) 的,一个是攻击 \(c_i = 0\) 的。显然我们总是会先进行第一阶段,再去进行第二阶段。
在第一阶段中,我们开一个小根堆,里面放所有剑的攻击值。每次尝试把最小攻击值的剑进行提升(提升一个更大的肯定不优),如果这个最小的已经不能杀死任何 \(c_i > 0\) 的怪物就直接退出,把它放进第二阶段。否则就把新剑塞进堆里。继续扩展。
当第一阶段结束之后,我们手上仍然是 \(n\) 把剑。我们以任意顺序遍历 \(c_i = 0\) 的怪物,每次找到最小的比他大的剑,能杀则杀,这样就是答案。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int n, m, a[N], r;
struct monster{
    int b, c;
    bool operator < (const monster &x) const{
        return b < x.b;
    }
}p[N];
multiset<int> s;
void solve(){
    cin >> n >> m;
    priority_queue<int, vector<int>, greater<int> > q;
    for(int i = 1; i <= n; ++i) cin >> a[i], q.push(a[i]);
    for(int i = 1; i <= m; ++i) cin >> p[i].b;
    for(int i = 1; i <= m; ++i) cin >> p[i].c;
    sort(p + 1, p + 1 + m);
    s.clear();
    int j = 0, ans = 0;
    while(!q.empty()){
        while(j <= m && p[j].c == 0) ++j;
        if(j > m) break;
        int x = q.top();
        // cout << x << ' ' << j << '\n';
        q.pop();
        if(x < p[j].b) s.insert(x); 
        else q.push(max(x, p[j].c)), ++j, ++ans;
    }
    while(!q.empty()) s.insert(q.top()), q.pop();
    for(int i = 1; i <= m; ++i){
        if(p[i].c > 0) continue; 
        auto it = s.lower_bound(p[i].b);
        if(it != s.end()) s.erase(it), ++ans;
    }
    cout << ans << '\n';
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

CF2164D Copy String

每次操作是在干什么呢?选一些位置不变,选一些位置变成上一个。所以实际上多次操作的影响是,一个字符可以覆盖它后面一段的位置。
那么我们从后往前遍历 \(t\) 中的字符,找到每个 \(t_i\) 之前最后面的那个 \(j\) 满足 \(s_j = t_i\)。还要注意由于覆盖的位置不能有交叉,因此我们必须从之前已经移动到的最前的位置开始向前找,找不到就无解了。
我们记按这样模拟的 \(t_i\) 对应的 \(s\) 中字符为 \(p_i\)。那么此时答案就是 \(\max(p_i - i)\)。或者反过来说,一个 \(s_i\) 覆盖的一定是一段区间,我们记这个右端点是 \(r_i\)。答案就是 \(\max(r_i - i)\)
考虑输出方案,对于 \(s\) 中每个位置维护一个指针,如果小于等于 \(r_i\) 就继续覆盖并右移指针即可。

Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
string s, t;
int n, k, p[N], r[N], pos[N];
void solve(){
    cin >> n >> k >> s >> t;
    memset(r, 0, sizeof(r));
    int j = n - 1;
    for(int i = n - 1; i >= 0; --i){
        if(j > i) --j;
        while(j >= 0 && s[j] != t[i]) --j;
        if(j == -1) return cout << -1 << '\n', void();
        p[i] = j; 
        r[j] = max(r[j], i);
    }
    int ans = 0;
    for(int i = 0; i < n; ++i){
        pos[i] = i;
        if(r[i]) ans = max(ans, r[i] - i);
    }
    if(ans > k) return cout << -1 << '\n', void();
    cout << ans << '\n';
    string f = s, g;
    for(int i = 1; i <= ans; ++i){
        g = f;
        for(int j = n - 1; j >= 0; --j){
            if(pos[j] < r[j]){
                pos[j]++;
                g[pos[j]] = s[j];
            } 
        }
        cout << g << '\n';
        f = g;
    }
}
int main(){
    cin.tie(0)->sync_with_stdio(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

CF2164E Journey

要求调整出一条欧拉回路,又是丁香之路。注意对于任意图我们都可以让奇度点配对来调整出欧拉回路,这是因为总度数 \(2m\) 一定是偶数。发现用 Mark 来调整不优于用 Transfer 来调整,那么我们加的那些边都按 Transfer 来看即可。然后 Transfer 的代价是最小瓶颈路的形式,按编号 Kruskal 重构树之后,每个点的点权是从根开始到这个点路径上的原来边权的最小值。因此我们总是在两个最深的地方对两个奇度点进行配对的代价最小,这样就做完了。

Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e6 + 5;
struct edge{
    int u, v, id, w;
    bool operator < (const edge &b) const {
        return id < b.id;
    }
}e[N];
int n, m, f[N], tot, a[N], deg[N];
int getf(int u){ return (f[u] == u ? u : f[u] = getf(f[u])); }
vector<int> tr[N];
ll ans;
int dfs(int u){
    int cnt = 0;
    if(u <= n) return deg[u] & 1;
    for(int v : tr[u]){
        a[v] = min(a[v], a[u]);
        cnt += dfs(v);
    }
    ans += (cnt / 2) * a[u];
    return cnt & 1;
}
void solve(){
    cin >> n >> m;
    tot = n;
    for(int i = 1; i <= 2 * n; ++i) f[i] = i, tr[i].clear(), a[i] = deg[i] = 0;
    ans = 0;
    for(int i = 1; i <= m; ++i){
        cin >> e[i].u >> e[i].v >> e[i].w, e[i].id = i;
        deg[e[i].u]++, deg[e[i].v]++;
        ans += e[i].w;
    }
    sort(e + 1, e + 1 + m);
    for(int i = 1; i <= m; ++i){
        int u = getf(e[i].u), v = getf(e[i].v), w = e[i].w;
        if(u == v){ a[u] = min(a[u], w); continue;}
        ++tot;
        tr[tot].emplace_back(u);
        tr[tot].emplace_back(v);
        a[tot] = w;
        f[u] = f[v] = tot;
    }
    dfs(tot);
    cout << ans << '\n';
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

CF2164F Chain Prefix Rank

广义串并联图,在 oi 中简而言之就是从一条边两个点不断通过串联和并联生成出来的无向图(听说还可以包含三元环,但是这是无法通过简单的串并联构造出来的)。其总是可以通过删一度点/缩二度点/叠合重边变成一个单点图。本题非常棒地展示了缩二度点在计数问题里的应用。题目保证有解,接下来我们只考虑有解的情况。
这个题意就很像一个拓扑序计数的问题。我们先造两个哨兵点 \(0, n + 1\)。让 \(0 \to n + 1, n + 1 \to 1\),并且让所有 \(a_i\) 全部加一,这样 0 就总是在这个序列的最开头,n + 1 就总是在序列的最末尾。我们发现,如果 \(a_i\) 都确定了,从 0 到 \(u\) 这条链上所有点的相对顺序都确定了。我们考虑构造这个相对顺序,首先 \(1\) 肯定是 \(0 \to 1 \to n + 1\)。然后对于一个 \(u\),比如从 \(0 \to fa_u\) 的相对顺序序列是 \(p\),那么到 \(u\) 的就是 \(0 \to \cdots, p_{a_u} \to u \to p_{a_u + 1} \cdots \to n + 1\)。当在满足 \(u\) 的所有祖先均合法的情况下,我们发现,只需要在加上 \(p_{a_u} \to u\),并且 \(u \to p_{a_u + 1}\) 的拓扑序限制即可,并且此时我们可以断言 \(p_{a_u}\) 本来就有一条边指向了 \(p_{a_u + 1}\)。这一建图过程可以用 FHQ 模拟。
发现我们现在要计数的拓扑序是在一个特殊的 DAG 上。由于这个图本身就是由并联操作生成的,因此我们现在希望通过反向缩二度点来求得答案。一开始,我们找到一些 \(u\),使得它只有一条入边并且也只有一条出边,假设是 \((s, u) \to (u, t)\),这个就非常类似于二度点,只是变成了一进一出的有向图。然后我们考虑把 \(u\) 并到 \((s, t)\) 上。具体来说,对每条边维护 \(dp_{(x, y)}\) 表示当前 \((x, y)\) 代表的子图的拓扑序方案和 \(siz_{(x, y)}\) 表示子图的大小。初始时 \(dp\) 为 1,\(siz\) 为 0。然后合并时的转移,由于 \((s, u)\) 上的点必须放在 \(u\) 之前,\(u\) 之后才能放 \((t, u)\) 的子图,所以这部分的拓扑序固定了,但是原来 \((s, t)\) 上的点跟这些点是并联关系,没有拓扑序限制,所以转移就是 \(dp_{(s, t)} \gets dp_{(s,t)} \times dp_{(s, u)} \times dp_{(u, t)} \times {siz_{(s, t)} + siz_{(s, u)} + siz_{(u, t)} + 1 \choose siz_{(s, t)}}\)\(siz\) 就是相加即可。转移顺序用拓扑排序钦定。
最后,整张图都被并到了 \((0, n + 1)\) 这条边上,因此答案是 \(dp_{(0, n + 1)}\)

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5, mod = 998244353;
typedef long long ll;
typedef pair<int, int> pii;
mt19937 rnd;
vector<int> e[N];
struct FHQ{
    int tot, rt;
    struct node{
        int key, ls, rs, siz, v;
    }tr[N];
    #define ls(p) tr[p].ls
    #define rs(p) tr[p].rs
    #define siz(p) tr[p].siz
    #define key(p) tr[p].key
    int newnode(int v){ return tr[++tot] = {(int)rnd(), 0, 0, 1, v}, tot; }
    void pushup(int p){ siz(p) = siz(ls(p)) + siz(rs(p)) + 1; }
    void clear(){ rt = tot = 0; }
    void split(int p, int x, int &L, int &R){ // L: <= x, R > x
        if(!p) return L = R = 0, void(); // Don't forget
        if(siz(ls(p)) >= x) R = p, split(ls(p), x, L, ls(p)); // split by size
        else L = p, split(rs(p), x - siz(ls(p)) - 1, rs(p), R);
        pushup(p);
    }
    int merge(int L, int R){
        if(!L || !R) return L | R;
        if(key(L) > key(R)){
            rs(L) = merge(rs(L), R);
            pushup(L);
            return L;
        }
        else{
            ls(R) = merge(L, ls(R));
            pushup(R);
            return R;
        }
    }
    void ins(int pos, int x){
        int L, R;
        split(rt, pos, L, R);
        rt = merge(merge(L, newnode(x)), R);
    }
    void erase(int pos){
        int L, R, m;
        split(rt, pos, L, R);
        split(L, pos - 1, L, m);
        rt = merge(L, R);
    }
    int get(int pos){
        int x, y, z; 
        split(rt, pos, x, y);
        split(x, pos - 1, x, z);
        rt = merge(merge(x, z), y);
        return tr[z].v;
    }
}s;
set<int> in[N], out[N];
map<pii, int> mp;
int n, a[N], cnt;
void adde(int u, int v){ ++cnt, out[u].emplace(v), in[v].emplace(u), mp[{u, v}] = cnt; }
ll siz[N], dp[N], fac[N], ifac[N], inv[N];
ll C(int n, int m){ return fac[n] * ifac[n - m] % mod * ifac[m] % mod; } 
void dfs(int u){
    int x = s.get(a[u]), y = s.get(a[u] + 1);
    adde(x, u), adde(u, y);
    s.ins(a[u], u);
    for(int v : e[u]) dfs(v);
    s.erase(a[u] + 1);
}
void solve(){
    cin >> n;
    for(int i = 0; i <= n + 1; ++i) in[i].clear(), out[i].clear(), e[i].clear();
    mp.clear();
    cnt = 0;
    s.clear();
    for(int i = 2; i <= n; ++i){
        int p; cin >> p;
        e[p].emplace_back(i);
    }
    for(int i = 1; i <= n; ++i) cin >> a[i], a[i]++;
    adde(0, n + 1); s.ins(0, 0), s.ins(1, n + 1);
    dfs(1);
    queue<int> q;
    for(int i = 1; i <= n; ++i) if(in[i].size() == 1 && out[i].size() == 1) q.push(i);
    for(int i = 1; i <= cnt; ++i) dp[i] = 1, siz[i] = 0;
    while(!q.empty()){
        int u = q.front(); q.pop();
        int x = *in[u].begin(), y = *out[u].begin();
        int s = mp[{x, u}], t = mp[{u, y}], p = mp[{x, y}];
        dp[p] = dp[p] * dp[s] % mod * dp[t] % mod * C(siz[s] + siz[t] + siz[p] + 1, siz[p]) % mod;
        siz[p] += siz[s] + siz[t] + 1;
        out[x].erase(u), in[y].erase(u);
        for(int v : {x, y}){
            if(in[v].size() == 1 && out[v].size() == 1) q.push(v);
        }
    }
    cout << dp[mp[{0, n + 1}]] << '\n';
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    inv[1] = fac[0] = fac[1] = ifac[0] = ifac[1] = 1;
    for(int i = 2; i <= 1e6; ++i){
        fac[i] = fac[i - 1] * i % mod;
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        ifac[i] = ifac[i - 1] * inv[i] % mod;
    }
    int T; cin >> T;
    while(T--) solve();
    return 0;
}
posted @ 2025-11-12 11:16  Hengsber  阅读(25)  评论(0)    收藏  举报