牛客周赛 Round 143 题解

由于牛客的渲染问题,你可以点此链接进入我的博客查看

已向 #define int long long 屈服

A 小红的区间构造

知识点:构造

直接输出区间 即可。这个区间里所有数都是正整数,且数量恰好为

时间复杂度

void solve() {
    int x;
    cin >> x;
    cout << 1 << " " << x << '\n';
}

B 小红的冷门副本

知识点:计数、哈希表

统计每个出现过的副本被选择的次数。没有出现过的副本次数为 ,一定会参与统计。

设出现次数大于 的副本有 个,它们不是冷门副本,因此答案为

时间复杂度

void solve() {
    int n, m, x;
    cin >> n >> m >> x;
    unordered_map<int, int> cnt;
    for (int i = 0; i < n; i++) {
        int a;
        cin >> a;
        cnt[a]++;
    }

    int hot = 0;
    for (auto [id, c] : cnt) {
        if (c > x) hot++;
    }
    cout << m - hot << '\n';
}

C 小红的因子幂和

知识点:质因数分解、约数枚举、快速幂

先对 分别质因数分解,把同一个质因子的指数相加,即可得到 的质因数分解。

接着 DFS 枚举 的所有正因子 ,对每个因子计算 并累加。

时间复杂度

void solve() {
    int x, y;
    cin >> x >> y;
    map<int, int> mp;
    auto fac = [&](int n) {
        for (int i = 2; i * i <= n; ++i) {
            while (n % i == 0) {
                mp[i]++;
                n /= i;
            }
        }
        if (n > 1) mp[n]++;
    };
    fac(x), fac(y);
    vector<int> d{1};
    for (auto [p, c] : mp) {
        int s = d.sz, cur = 1;
        for (int i = 1; i <= c; ++i) {
            cur *= p;
            for (int j = 0; j < s; ++j) d.pb(d[j] * cur);
        }
    }
    Z ans = 0;
    for (auto v : d) ans += mypow(Z(v), v);
    cout << ans << endl;
}

D 小红的最佳区间

知识点:区间转化、扫描线

给定区间 与选择的 相交,当且仅当:

也就是 。于是每个原区间都会转化成一个可选 的区间,问题变成求若干区间覆盖同一个点的最大数量。

对所有 做扫描线即可。因为是闭区间,所以在左端点加 ,在右端点 的位置减

时间复杂度

void solve() {
    int n, k;
    cin >> n >> k;

    vector<PII> e;

    FOR(i, 1, n) {
        int l, r;
        cin >> l >> r;
        e.pb({l - k, 1}), e.pb({r + 1, -1});
    }

    sort(all(e));

    int ans = 0, cur = 0;
    for (int i = 0; i < e.sz;) {
        int x = e[i].fi;
        while (i < e.sz && e[i].fi == x) cur += e[i].se, i++;
        ans = max(ans, cur);
    }

    cout << ans << endl;
}

E 小红的好矩阵

知识点:构造、分类讨论、分块

每个连通块大小都必须恰好为 ,所以总格子数 必须能被 整除,即 必须是 的倍数,否则无解。

把矩阵按每 列分成一块。一个 块内要被分成两个大小为 的连通块,典型形态只有横条形和两种 L 形。

L 形块之间不会跨块连通,因此每个块可以独立取两种翻转方向的较小代价;横条形会沿相邻块横向连通,所以必须按块交替颜色,只需枚举第一块是 000/111 还是 111/000。四类方案取最小值即可。

时间复杂度

void solve() {
    int n;
    cin >> n;
    string s1, s2;
    cin >> s1 >> s2;

    if (n % 3 != 0) {
        cout << -1 << endl;
        return;
    }

    int m = n / 3;

    int costA = 0, costB = 0, costC0 = 0, costC1 = 0;

    auto calc = [&](int l, string top, string bot) -> int {
        int c = 0;
        for (int k = 0; k < 3; k++) {
            if (s1[l + k] != top[k]) c++;
            if (s2[l + k] != bot[k]) c++;
        }
        return c;
    };

    for (int i = 0; i < m; i++) {
        int l = i * 3;

        int a1 = calc(l, "001", "011");
        int a2 = calc(l, "011", "001");

        int b1 = calc(l, "100", "110");
        int b2 = calc(l, "110", "100");

        int c1 = calc(l, "000", "111");
        int c2 = calc(l, "111", "000");

        costA += min(a1, a2);
        costB += min(b1, b2);

        if (i % 2 == 0)
            costC0 += c1, costC1 += c2;
        else
            costC0 += c2, costC1 += c1;
    }

    cout << min({costA, costB, costC0, costC1}) << endl;
}

知识点:状态压缩 DP、轮廓线、并查集

仍然先判 是否为 的倍数。按列做轮廓线 DP。每一列只有 种颜色状态,状态记录最后一列颜色,以及最后一列上、下两个格子所在连通块当前大小。转移到下一列时,用并查集合并两列共 个格子中同色且共边的位置;若某个连通块已经离开轮廓线,它之后不可能再扩展,大小必须立刻等于 ,仍留在轮廓线上的连通块大小不能超过 。最后检查轮廓线上剩余连通块是否都为

时间复杂度

struct DSU {
    vector<int> f, siz;
    DSU() {}
    DSU(int n) {
        init(n);
    }
    void init(int n) {
        f.resize(n + 1);
        iota(f.begin(), f.end(), 0);
        siz.assign(n + 1, 1);
    }
    int find(int x) {
        return f[x] == x ? x : f[x] = find(f[x]);
    }
    void merge(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return;
        f[y] = x;
        siz[x] += siz[y];
    }
};

void solve() {
    int n;
    cin >> n;
    vector<string> s(2);
    cin >> s[0] >> s[1];
    if (n % 3) {
        cout << -1 << endl;
        return;
    }
    auto id = [&](int m, int a, int b) { return m * 16 + a * 4 + b; };
    auto cost = [&](int i, int m) {
        int res = 0;
        res += (s[0][i] - '0' != (m & 1));
        res += (s[1][i] - '0' != ((m >> 1) & 1));
        return res;
    };
    auto go = [&](int pm, int a, int b, int nm) {
        DSU d(3);
        int c[4] = {pm & 1, (pm >> 1) & 1, nm & 1, (nm >> 1) & 1};
        if (c[0] == c[1]) d.merge(0, 1);
        if (c[0] == c[2]) d.merge(0, 2);
        if (c[1] == c[3]) d.merge(1, 3);
        if (c[2] == c[3]) d.merge(2, 3);
        int siz[4] = {};
        if (c[0] == c[1]) {
            siz[d.find(0)] += a;
        } else {
            siz[d.find(0)] += a;
            siz[d.find(1)] += b;
        }
        siz[d.find(2)]++;
        siz[d.find(3)]++;
        FOR(i, 0, 3) if (siz[i] > 3) return array<int, 3>{-1, -1, -1};
        int r0 = d.find(2), r1 = d.find(3);
        FOR(i, 0, 3) {
            if (d.find(i) == i && siz[i] && i != r0 && i != r1 && siz[i] != 3) {
                return array<int, 3>{-1, -1, -1};
            }
        }
        return array<int, 3>{1, siz[r0], siz[r1]};
    };
    vector<int> dp(64, INF), ndp(64, INF);
    FOR(m, 0, 3) {
        int a = ((m & 1) == ((m >> 1) & 1) ? 2 : 1);
        int b = a;
        dp[id(m, a, b)] = cost(0, m);
    }
    FOR(i, 1, n - 1) {
        fill(all(ndp), INF);
        FOR(st, 0, 63) {
            if (dp[st] == INF) continue;
            int pm = st / 16, a = st % 16 / 4, b = st % 4;
            FOR(nm, 0, 3) {
                auto t = go(pm, a, b, nm);
                if (t[0] == -1) continue;
                cmin(ndp[id(nm, t[1], t[2])], dp[st] + cost(i, nm));
            }
        }
        dp.swap(ndp);
    }
    int ans = INF;
    FOR(st, 0, 63) {
        if (dp[st] == INF) continue;
        int m = st / 16, a = st % 16 / 4, b = st % 4;
        if ((m & 1) == ((m >> 1) & 1)) {
            if (a == 3) cmin(ans, dp[st]);
        } else {
            if (a == 3 && b == 3) cmin(ans, dp[st]);
        }
    }
    cout << (ans == INF ? -1 : ans) << endl;
}

F 小红的网格路径 II

知识点:动态规划、分段贡献、快速幂

只维护当前最近一个障碍行 上方和下方的单位贡献,记为 updown。遇到下一列障碍 时:

  • 若中间隔着空列,先把当前总方案数扩散到所有行,再乘上空列数量对应的 的幂;
  • 若两个障碍在相邻列,则直接按 的相对位置,把上方、下方贡献重新拆分。

这样每个障碍只处理一次。最后若最后一个障碍就在第 列,只能从它下方到达终点;否则再经过若干空列扩散到终点列。

时间复杂度

void solve() {
    int n, m, k;
    cin >> n >> m >> k;

    vector<PII> a(k);
    for (auto &[y, x] : a) cin >> x >> y;
    sort(all(a));

    if (k == 0) {
        cout << mypow(Z(n), m - 1) << endl;
        return;
    }

    auto step = [&](int px, int py, Z up, Z down, int nx, int ny) {
        int gap = ny - py - 1;
        if (gap >= 1) {
            Z sum = Z(px - 1) * up + Z(n - px) * down;
            Z v = sum * mypow(Z(n), gap - 1);
            return pair<Z, Z>{Z(nx - 1) * v, Z(n - nx) * v};
        } else {
            if (nx < px) {
                Z nu = Z(nx - 1) * up;
                Z nd = Z(px - nx - 1) * up + Z(n - px) * down;
                return pair<Z, Z>{nu, nd};
            } else if (nx == px) {
                Z nu = Z(px - 1) * up;
                Z nd = Z(n - px) * down;
                return pair<Z, Z>{nu, nd};
            } else {
                Z nu = Z(px - 1) * up + Z(nx - px - 1) * down;
                Z nd = Z(n - nx) * down;
                return pair<Z, Z>{nu, nd};
            }
        }
    };

    int x = a[0].se, y = a[0].fi;
    Z up, down;

    if (y == 1) {
        up = 1;
        down = 0;
    } else {
        Z v = mypow(Z(n), y - 2);
        up = Z(x - 1) * v;
        down = Z(n - x) * v;
    }

    int px = x, py = y;

    for (int i = 1; i < k; ++i) {
        int nx = a[i].se, ny = a[i].fi;
        auto [nu, nd] = step(px, py, up, down, nx, ny);
        up = nu;
        down = nd;
        px = nx;
        py = ny;
    }

    Z ans;
    if (py == m) {
        ans = down;
    } else {
        Z sum = Z(px - 1) * up + Z(n - px) * down;
        ans = sum * mypow(Z(n), m - py - 1);
    }

    cout << ans << endl;
}

知识点:动态规划、线段树、坐标压缩

路径不能向左走,因此每一列只会走一段竖直线,再向右进入下一列。设进入某列的行分布为 DP 值,若这一列没有障碍,则所有行的新值都等于上一列所有行的方案和;若这一列障碍在第 行,则 不能走,障碍上方所有行的新值等于上方方案和,障碍下方所有行的新值等于下方方案和。

由于每列至多一个障碍,DP 值只会在障碍行附近发生分段变化。把所有障碍行和起点行 取出做坐标压缩,用线段树维护每个行段的长度和方案和,支持整段赋值与区间求和。连续空列的转移可以一次性用快速幂合并。

时间复杂度

void solve() {
    int n, m, k;
    cin >> n >> m >> k;
    vector<PII> a(k);
    vector<int> xs;
    xs.pb(1);
    for (auto &[y, x] : a) {
        cin >> x >> y;
        xs.pb(x);
    }
    sort(all(a)), sort(all(xs));
    xs.erase(unique(all(xs)), xs.end());
    vector<PII> seg;
    map<int, int> id;
    int lst = 0;
    for (auto x : xs) {
        if (lst + 1 <= x - 1) seg.pb({lst + 1, x - 1});
        id[x] = seg.sz + 1;
        seg.pb({x, x});
        lst = x;
    }
    if (lst + 1 <= n) seg.pb({lst + 1, n});
    int S = seg.sz;
    vector<int> len(S + 1);
    for (int i = 1; i <= S; ++i) {
        len[i] = seg[i - 1].se - seg[i - 1].fi + 1;
    }
    SegTree<Z> tr(S);
    tr.build(len);
    tr.update(id[1], id[1], {1});
    auto full = [&](int d) {
        if (d <= 0) return;
        Z sum = tr.ask(1, S).val;
        tr.update(1, S, {sum * mypow(Z(n), d - 1)});
    };
    auto block = [&](int x) {
        int p = id[x];
        Z l = tr.ask(1, p - 1).val;
        Z r = tr.ask(p + 1, S).val;
        tr.update(1, p - 1, {l});
        tr.update(p, p, {0});
        tr.update(p + 1, S, {r});
    };
    int cur = 1;
    for (auto [y, x] : a) {
        full(y - cur);
        if (y == m) {
            int p = id[x];
            cout << tr.ask(p + 1, S).val << endl;
            return;
        }
        block(x);
        cur = y + 1;
    }
    full(m - cur);
    cout << tr.ask(1, S).val << endl;
}