跳转至

GESP 7级

P10265 [GESP样题 七级] 迷宫统计

给出迷宫之间能否直接到达的关系。
统计m能直接到达的迷宫数、能直接到达m的迷宫数,以及二者之和。
提示1-看清“直接”

30分做法,\(n\le 10\)。

另外30分做法,\(n\le 100\)。

看全部数据范围,\(n\le 1000\)。

这里只问直接到达,不需要搜索路径。

提示2-看哪一行、哪一列

第 \(m\) 行表示从 \(m\) 出发,第 \(m\) 列表示到达 \(m\)。

自己到自己也要计数。

最后输出的是两个数量之和,不是去重后的数量。

正在读第i行、第j列的输入x时,只需维护两个计数。对角线也分别属于这一行和这一列:

if (i == m) out += x;
if (j == m) in += x;
代码-满分-边读边统计

\(O(n^{2})\) 时间,\(O(1)\) 额外空间。输入本身就有 \(n^{2}\) 个数,无需另写部分分。

#include <bits/stdc++.h>

using namespace std;

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

    int n, m, out = 0, in = 0;

    cin >> n >> m;

    for (int i = 1; i <= n; i++) {
        for (int j = 1, x; j <= n; j++) {
            cin >> x;
            if (i == m) out += x;
            if (j == m) in += x;
        }
    }

    cout << out << ' ' << in << ' ' << out + in;

    return 0;
}

P10287 [GESP样题 七级] 最长不下降子序列

在有向无环图中选择一条路径。
求路径点权序列的最长不下降子序列长度的最大值。
提示1-先看特殊性质

30分做法,\(n\le 1000\),图是一条链。

另外30分做法,点权\(\le 2\)。

看全部数据范围,\(n,m\le 10^{5}\),点权\(\le 10\)。

链上就是普通的最长不下降子序列,可以用平方级 DP。

注意链的顺序不一定是编号顺序。

提示2-点权只有十种

走到同一个点时,只需记录:最后选的点权是几,最多选了几个点。

设 \(\operatorname{dp}_{u,v}\) 表示走到 \(u\),最后选的权值为 \(v\) 的最大长度。

\(v=0\) 表示还没选。

提示3-选或不选

按拓扑顺序处理 \(u\)。

可以跳过 \(u\),也可以从最后权值不超过 \(a_{u}\) 的状态中选上 \(u\)。

再把所有状态传给后继。

不可达状态设为 -1,允许从任意点开始。

轮到拓扑序中的u时,先找末值不超过a[u]的最佳状态。只接上一轮状态,当前节点恰好选一次:

int best = 0;
for (int v = 0; v <= a[u]; v++) best = max(best, dp[u][v]);
dp[u][a[u]] = max(dp[u][a[u]], best + 1);
代码-30分-小图 DP

\(O(n(n+m))\) 时间,\(O(n+m)\) 空间。链上 \(m=n-1\),即 \(O(n^{2})\),覆盖 30 分子任务。只有可达的点才能接在同一条路径上。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

vector<int> g[N];
int a[N], deg[N], dp[N];
bool vis[N];
int order[N];

int main() {
    int n, m;

    cin >> n >> m;

    fill(a, a + n + 1, 0);
    fill(deg, deg + n + 1, 0);
    int len = 0;

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

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
        deg[v]++;
    }

    queue<int> q;
    for (int u = 1; u <= n; u++) if (deg[u] == 0) q.push(u);

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        order[len++] = u;
        for (int v : g[u]) {
            deg[v]--;
            if (deg[v] == 0) q.push(v);
        }
    }

    // dp[u]:最后选中u的最长不下降子序列长度。
    fill(dp, dp + (n + 1), 1);
    int ans = 1;

    for (int z = 0; z < len; z++) {
        int u = order[z];
        ans = max(ans, dp[u]);
        fill(vis, vis + (n + 1), 0);
        q.push(u);
        vis[u] = true;

        while (!q.empty()) {
            int x = q.front();
            q.pop();
            for (int v : g[x]) if (!vis[v]) {
                vis[v] = true;
                q.push(v);
                // 中间点可以跳过,但下一个选中的点必须能从u到达。
                if (a[u] <= a[v]) dp[v] = max(dp[v], dp[u] + 1);
            }
        }
    }

    cout << ans;

    return 0;
}
代码-满分-拓扑 DP

\(O(10(n+m))\) 时间,\(O(10n+m)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N], deg[N];

int dp[N][11];

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

    int n, m;

    cin >> n >> m;

    fill(a, a + (n + 1), 0);
    fill(deg, deg + (n + 1), 0);

    for (int u = 1; u <= n; u++) {
        cin >> a[u];
        for (int v = 0; v <= 10; v++) dp[u][v] = -1;
        dp[u][0] = 0;
    }

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
        deg[v]++;
    }

    queue<int> q;

    for (int u = 1; u <= n; u++) if (!deg[u]) q.push(u);

    int ans = 0;

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        int best = 0;
        for (int v = 0; v <= a[u]; v++) best = max(best, dp[u][v]);
        dp[u][a[u]] = max(dp[u][a[u]], best + 1);
        for (int v = 0; v <= 10; v++) ans = max(ans, dp[u][v]);
        for (int to : g[u]) {
            for (int v = 0; v <= 10; v++) dp[to][v] = max(dp[to][v], dp[u][v]);
            deg[to]--;
            if (deg[to] == 0) q.push(to);
        }
    }

    cout << ans;

    return 0;
}

P10110 [GESP202312 七级] 商品交易

按指定方向交换商品,每次补价值差,再交1元手续费。
从商品a换到商品b,求最少花费;不能到达则输出No solution。
提示1-先看范围

30分做法,\(N\le 10\)、\(M\le 20\)。

70分做法,\(N\le 1000\)、\(M\le 10^{4}\)。

看全部数据范围,\(N\le 10^{5}\)、\(M\le 2\times 10^{5}\)。

小图可以用邻接矩阵搜索,大图要用邻接表。

提示2-价值差会怎样

走过 \(a\to u\to b\),价值差相加后,中间商品的价值会抵消。

无论走哪条路线,总花费都是 \(v_b-v_a\) 加上交易次数。

提示3-只求最少次数

把每次交易看成一条有向边,用 BFS 求最少边数。

不要把交易当成双向的。

价值差可能很大,用 long long 算答案。

BFS中d[a]为0,其余初始-1。沿一条允许交易的边u到v,第一次到达就得到最少交易次数:

if (d[v] == -1) {
    d[v] = d[u] + 1;
    q.push(v);
}
代码-70分-邻接矩阵 BFS

\(O(N^{2}+M)\) 时间,\(O(N^{2})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

long long value[N];
int d[N];

bool g[N][N];

int main() {
    int n, m, a, b;

    cin >> n >> m >> a >> b;

    fill(value, value + (n), 0);

    for (int z = 0; z < n; z++) cin >> value[z];

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u][v] = true;
    }

    fill(d, d + (n), -1);
    queue<int> q;
    d[a] = 0;
    q.push(a);

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v = 0; v < n; v++) if (g[u][v] && d[v] == -1) {
            d[v] = d[u] + 1;
            q.push(v);
        }
    }

    if (d[b] == -1) cout << "No solution";
    else cout << value[b] - value[a] + d[b];

    return 0;
}
代码-满分-邻接表 BFS

\(O(N+M)\) 时间与空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
long long value[N];
int d[N];

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

    int n, m, a, b;

    cin >> n >> m >> a >> b;

    fill(value, value + (n), 0);

    for (int z = 0; z < n; z++) cin >> value[z];

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
    }

    fill(d, d + (n), -1);
    queue<int> q;
    d[a] = 0;
    q.push(a);

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u]) if (d[v] == -1) {
            d[v] = d[u] + 1;
            q.push(v);
        }
    }

    if (d[b] == -1) cout << "No solution";
    else cout << value[b] - value[a] + d[b];

    return 0;
}

P10111 [GESP202312 七级] 纸牌游戏

已知对手每轮出的牌,赢得2a[i]分,平局得a[i]分。
第t次换牌扣b[t]分,求最高总得分。
提示1-先看范围

30分做法,\(N\le 15\)。

60分做法,\(N\le 100\)。

看全部数据范围,\(N\le 1000\),\(a_{i},b_{i}\le 10^{6}\)。

逐轮枚举三种牌有 \(3^{N}\) 种计划,\(N=15\) 时可拿部分分。

提示2-罚分跟什么有关

换牌扣多少分,取决于这是第几次换牌,不是当前第几轮。

设 \(\operatorname{dp}_{t,c}\) 为上一轮结束时,换过 \(t\) 次、最后出 \(c\) 的最高分。

提示3-继续还是换牌

继续出 \(c\),不增加换牌次数。

从另一张牌换到 \(c\),次数加一并扣相应的 \(b\)。

每轮再加上本轮得分。

滚动数组后,时间 \(O(N^{2})\),空间 \(O(N)\)。

外层已经固定本轮i、旧状态t和last,正在试新牌card。nt是实际换牌总次数,罚的是第nt次的分:

int nt = t + (card != last);
long long cost = card != last ? b[nt] : 0;
ndp[nt][card] = max(ndp[nt][card], dp[t][last] + gain(i, card) - cost);
代码-30分-枚举出牌

\(O(3^{N})\) 时间,\(O(N)\) 空间。

#include <bits/stdc++.h>

using namespace std;

int n;
long long a[20], b[20];
int c[20];
long long ans = 0;
long long gain(int i, int card) {

    if (card == c[i]) return a[i];
    return (card + 2) % 3 == c[i] ? 2 * a[i] : 0;
}
void dfs(int i, int last, int changes, long long score) {

    if (i == n) {
        ans = max(ans, score);
        return;
    }

    for (int card = 0; card < 3; card++) {
        int t = changes + (i > 0 && card != last);
        long long cost = t > changes ? b[t] : 0;
        dfs(i + 1, card, t, score + gain(i, card) - cost);
    }
}

int main() {
    cin >> n;

    for (int i = 0; i < n; i++) cin >> a[i];

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

    for (int i = 0; i < n; i++) cin >> c[i];
    dfs(0, -1, 0, 0);

    cout << ans;

    return 0;
}
代码-满分-换牌次数 DP

\(O(N^{2})\) 时间,\(O(N)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;
const long long NEG = -(1LL << 60);

long long a[N], b[N], dp[N][3], ndp[N][3];
int c[N];

long long gain(int i, int card) {
    if (card == c[i]) return a[i];

    return (card + 2) % 3 == c[i] ? 2 * a[i] : 0;
}

int main() {
    int n;

    cin >> n;

    for (int i = 0; i < n; i++) cin >> a[i];

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

    for (int i = 0; i < n; i++) cin >> c[i];

    for (int t = 0; t < n; t++)
        for (int card = 0; card < 3; card++) dp[t][card] = NEG;

    for (int card = 0; card < 3; card++) dp[0][card] = gain(0, card);

    for (int i = 1; i < n; i++) {
        for (int t = 0; t < n; t++)
            for (int card = 0; card < 3; card++) ndp[t][card] = NEG;
        for (int t = 0; t < i; t++) for (int last = 0; last < 3; last++) {
            if (dp[t][last] == NEG) continue;
            for (int card = 0; card < 3; card++) {
                int nt = t + (card != last);
                long long cost = card != last ? b[nt] : 0;
                ndp[nt][card] = max(ndp[nt][card], dp[t][last] + gain(i, card) - cost);
            }
        }
        for (int t = 0; t < n; t++)
            for (int card = 0; card < 3; card++) dp[t][card] = ndp[t][card];
    }

    long long ans = 0;

    for (int t = 0; t < n; t++)
        for (int card = 0; card < 3; card++) ans = max(ans, dp[t][card]);

    cout << ans;

    return 0;
}

P10378 [GESP202403 七级] 交流问题

每次交流的两名同学来自不同学校。
不知道各人的学校,求B校人数的最小值和最大值。
提示1-先看范围

30分做法,\(n\le 17\)、\(m\le 50\)。

60分做法,\(n\le 500\)、\(m\le 2000\)。

看全部数据范围,\(n\le 10^{5}\)、\(m\le 2\times 10^{5}\)。

\(n\le 17\) 时,可以枚举每个人属于哪所学校。

提示2-朋友必须分两边

把交流关系连成无向边。

沿边给两端染不同颜色,得到每个连通块的两类同学。

题目保证交流合法,不会出现染色矛盾。

提示3-每块可以单独翻转

一个连通块两种颜色有 \(x\)、\(y\) 人。

哪种颜色是 \(B\) 校,都有可能。

最少贡献 \(\min(x,y)\),最多贡献 \(\max(x,y)\)。

各块相加。

孤立点也要单独处理。

一个连通块染色完成后,cnt[0]和cnt[1]记录两边人数。给哪边贴B校标签可以独立选择:

low += min(cnt[0], cnt[1]);
high += max(cnt[0], cnt[1]);
代码-30分-枚举学校

\(O(2^{n}(n+m))\) 时间,\(O(n+m)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 200005;

pair<int, int> edges[N];

int main() {
    int n, m;

    cin >> n >> m;

    for (int z = 0; z < m; z++) {
        int &u = edges[z].first, &v = edges[z].second;
        cin >> u >> v;
        u--;
        v--;
    }

    int low = n, high = 0;

    for (int mask = 0; mask < (1 << n); mask++) {
        bool ok = true;
        for (int z = 0; z < m; z++) {
            int u = edges[z].first, v = edges[z].second;
            if (((mask >> u) & 1) == ((mask >> v) & 1)) {
                ok = false;
                break;
            }
        }
        if (!ok) continue;
        int cnt = 0;
        for (int i = 0; i < n; i++) cnt += (mask >> i) & 1;
        low = min(low, cnt);
        high = max(high, cnt);
    }

    cout << low << ' ' << high;

    return 0;
}
代码-满分-二分图染色

\(O(n+m)\) 时间与空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int color[N];

int cnt[2];

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

    int n, m;

    cin >> n >> m;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    fill(color, color + (n + 1), -1);
    int low = 0, high = 0;

    for (int s = 1; s <= n; s++) if (color[s] == -1) {
        cnt[0] = cnt[1] = 0;
        queue<int> q;
        q.push(s);
        color[s] = 0;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            cnt[color[u]]++;
            for (int v : g[u]) if (color[v] == -1) {
                color[v] = color[u] ^ 1;
                q.push(v);
            }
        }
        low += min(cnt[0], cnt[1]);
        high += max(cnt[0], cnt[1]);
    }

    cout << low << ' ' << high;

    return 0;
}

P10379 [GESP202403 七级] 俄罗斯方块

相邻的同色格子构成一个方块。
只允许平移,不允许旋转或翻转;忽略颜色,求不同形状的数量。
提示1-先看特殊性质

30分做法,\(n,m\le 20\),方块不超过 \(5\times 5\)。

另外30分做法,\(n,m\le 500\),方块都是横条或竖条。

看全部数据范围,\(n,m\le 500\)。

条形方块只需记录高和宽。

长度相同的横条、竖条仍是不同形状。

提示2-先找到每一块

用 BFS 找出同色连通块,收集所有格子的坐标。

平移以后,坐标会一起变化。

怎样让同一种形状得到同一份记录?

提示3-让坐标从零开始

分别减去最小行号、最小列号,再排序。

把坐标列表放进 set 去重。

每块格子数不同,坐标列表保留vector;形状去重保留set,比开一个大二维数组存所有形状更直接。

这样只消除了平移,颜色不用记录,旋转后的形状不会被误合并。

cells收集当前块的坐标,up、l分别是最小行、最小列。先统一平移,再排序,位置不同但形状相同的块就得到相同记录:

for (int k = 0; k < (int)cells.size(); k++) {
    cells[k].first -= up;
    cells[k].second -= l;
}
sort(cells.begin(), cells.end());
代码-30分-条形子任务

令 \(B\) 为方块数,时间 \(O(nm+B\log B)\),空间 \(O(nm)\)。仅适用于条形子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

int a[N][N];
bool vis[N][N];
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};

int main() {
    int n, m;

    cin >> n >> m;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) cin >> a[i][j];
    }

    set<pair<int, int>> shapes;

    for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (!vis[i][j]) {
        int up = i, down = i, l = j, r = j;
        queue<pair<int, int>> q;
        q.push({i, j});
        vis[i][j] = true;
        while (!q.empty()) {
            int x = q.front().first, y = q.front().second;
            q.pop();
            up = min(up, x);
            down = max(down, x);
            l = min(l, y);
            r = max(r, y);
            for (int d = 0; d < 4; d++) {
                int nx = x + dx[d], ny = y + dy[d];
                if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny] && a[nx][ny] == a[i][j]) {
                    vis[nx][ny] = true;
                    q.push({nx, ny});
                }
            }
        }
        shapes.insert({down - up + 1, r - l + 1});
    }

    cout << shapes.size();

    return 0;
}
代码-满分-坐标归一化

令 \(K=nm\),时间 \(O(K\log K)\),空间 \(O(K)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

int a[N][N];
bool vis[N][N];
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};

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

    int n, m;

    cin >> n >> m;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) cin >> a[i][j];
    }

    set<vector<pair<int, int>>> shapes;

    for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (!vis[i][j]) {
        vector<pair<int, int>> cells;
        int up = i, l = j;
        queue<pair<int, int>> q;
        q.push({i, j});
        vis[i][j] = true;
        while (!q.empty()) {
            int x = q.front().first, y = q.front().second;
            q.pop();
            cells.push_back({x, y});
            up = min(up, x);
            l = min(l, y);
            for (int d = 0; d < 4; d++) {
                int nx = x + dx[d], ny = y + dy[d];
                if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny] && a[nx][ny] == a[i][j]) {
                    vis[nx][ny] = true;
                    q.push({nx, ny});
                }
            }
        }
        for (int k = 0; k < (int)cells.size(); k++) {
            cells[k].first -= up;
            cells[k].second -= l;
        }
        sort(cells.begin(), cells.end());
        shapes.insert(cells);
    }

    cout << shapes.size();

    return 0;
}

P10723 [GESP202406 七级] 黑白翻转

可以把白点变黑。求最少翻转几次,才能让剩下的黑点连成一棵树。
提示1-先看特殊性质

30分做法,树是一条链。

另外30分做法,恰有两个黑点。

看全部数据范围,\(n\le 10^{5}\)。

链上只需填满最左、最右黑点之间的白点。

两个黑点时,只需填满它们之间的路径。

提示2-哪些白点用不到

原来的黑点不能删除,它们之间的路径也必须保留。

没有黑点的末端分支,可以整个剪掉。

提示3-不断剪白色叶子

把白色叶子放入队列,删除后,新的白色叶子继续入队。

剩下的白点就是必须翻转的点。

如果原来一个黑点也没有,翻转任意一个点即可,答案为 1。

删除一个白叶子u后,只改变未删除邻居v的度数。v也变成白叶子时马上入队并标记,避免重复入队:

deg[v]--;
if (!a[v] && deg[v] <= 1) {
    removed[v] = true;
    q.push(v);
}
代码-30分-链形子任务

\(O(n)\) 时间与空间。按树上的真实顺序找两端黑点。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N];

int main() {
    int n;

    cin >> n;

    fill(a, a + (n + 1), 0);
    int black = 0;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        black += a[i];
    }

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

    if (!black) {
        cout << 1;

        return 0;
    }

    int u = 1;

    while (g[u].size() > 1) u++;
    int prev = 0, pos = 0, first = -1, last = -1;

    while (u) {
        if (a[u]) {
            if (first == -1) first = pos;
            last = pos;
        }
        int next = 0;
        for (int v : g[u]) if (v != prev) next = v;
        prev = u;
        u = next;
        pos++;
    }

    cout << last - first + 1 - black;

    return 0;
}
代码-满分-剪去白色叶子

\(O(n)\) 时间与空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N], deg[N];
bool removed[N];

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

    int n;

    cin >> n;

    fill(a, a + (n + 1), 0);
    fill(deg, deg + (n + 1), 0);
    fill(removed, removed + (n + 1), 0);
    int black = 0;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        black += a[i];
    }

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

    if (!black) {
        cout << 1;

        return 0;
    }

    queue<int> q;

    for (int u = 1; u <= n; u++) if (!a[u] && deg[u] <= 1) {
        q.push(u);
        removed[u] = true;
    }

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u]) if (!removed[v]) {
            deg[v]--;
            if (!a[v] && deg[v] <= 1) {
                removed[v] = true;
                q.push(v);
            }
        }
    }

    int ans = 0;

    for (int u = 1; u <= n; u++) if (!removed[u] && !a[u]) ans++;

    cout << ans;

    return 0;
}

P10724 [GESP202406 七级] 区间乘积

给定正整数数组,统计乘积为完全平方数的连续区间数量。
提示1-先看范围

20分做法,\(n\le 10^{5}\),\(a_{i}\le 2\)。

另外40分做法,\(n\le 100\),\(a_{i}\le 30\)。

看全部数据范围,\(n\le 10^{5}\),\(a_{i}\le 30\)。

乘积很快会溢出。

平方数的每个质因子,出现次数有什么特点?

提示2-只记录奇偶

平方数中,每个质因子的次数都是偶数。

30 以内只有 10 个质数,可用 10 个二进制位记录奇偶。

区间相乘,对应这些位做异或。

只有 1、2 时,只需记录 2 的次数奇偶。

提示3-变成前缀计数

区间异或为 0,等价于两端的前缀异或相同。

边扫描边统计相同前缀出现次数。

先放入空前缀 0,答案用 long long。

masks[x]记录x的质因数次数奇偶。读到一个新数时,先统计旧前缀,再把新前缀加入计数:

prefix ^= masks[x];
ans += cnt[prefix];
cnt[prefix]++;
代码-40分-小范围子任务

\(O(n^{2}+10n)\) 时间,\(O(n)\) 空间;仅用于 \(n\le 100\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int a[N];

int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};

int getmask(int x) {
    int mask = 0;

    for (int i = 0; i < 10; i++) while (x % prime[i] == 0) {
        x /= prime[i];
        mask ^= 1 << i;
    }

    return mask;
}

int main() {
    int n;

    cin >> n;

    fill(a, a + (n), 0);

    for (int z = 0; z < n; z++) {
        int &x = a[z];
        cin >> x;
        x = getmask(x);
    }

    long long ans = 0;

    for (int l = 0; l < n; l++) {
        int mask = 0;
        for (int r = l; r < n; r++) {
            mask ^= a[r];
            ans += mask == 0;
        }
    }

    cout << ans;

    return 0;
}
代码-满分-前缀异或

\(O(10n)\) 时间,\(O(2^{10})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 1024;

int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int masks[31];
long long cnt[N];

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

    for (int x = 1; x <= 30; x++) {
        int v = x;
        for (int i = 0; i < 10; i++) while (v % prime[i] == 0) {
            v /= prime[i];
            masks[x] ^= 1 << i;
        }
    }

    int n, prefix = 0;

    cin >> n;

    long long ans = 0;
    cnt[0] = 1;

    for (int i = 0, x; i < n; i++) {
        cin >> x;
        prefix ^= masks[x];
        ans += cnt[prefix];
        cnt[prefix]++;
    }

    cout << ans;

    return 0;
}

P11248 [GESP202409 七级] 矩阵移动

原题解由陈奕涵提供,以下整理了提示与示范代码。

在01? 网格中从左上走到右下,只能向右或向下。
最多把x个 ? 变成1,求路径上的最大得分,起终点也计分。
提示1-先看范围

30分做法,\(n,m\le 10\),\(x=1\)。

另外30分做法,\(n,m\le 500\),\(x\le 30\)。

看全部数据范围,\(n,m\le500\),\(x\le 300\),所有网格面积之和\(\le 2.5\times 10^{5}\)。

小网格可以枚举路径,记下经过多少个 1 和 ?。

提示2-问号不一定全选

设 \(\operatorname{dp}_{i,j,k}\) 为走到 \((i,j)\)、恰好修改 \(k\) 个问号时的最高分。

遇到 ? 可以不改,也可以花一次修改机会加一分。

不要把“经过的问号数”限制为 \(x\)。

提示3-从哪里走来

每个格子只会从上方或左方走来,取较大的分数,再处理当前字符。

按行滚动,最后取所有 \(k\le x\) 的最大值。

不可达状态要设成负无穷。

处理格子(i,j)和已修改k次的状态,dp[j]来自上方,dp[j-1]来自左方。先合并前驱,再把本格的贡献加上:

int best = max(dp[j][k], dp[j - 1][k]);
if (best == NEG) continue;
cur[k] = max(cur[k], best + (g[i - 1][j - 1] == '1'));
代码-30分-枚举路径

\(O(2^{n+m})\) 时间,\(O(n+m+nm)\) 空间。仅用于小网格子任务。

#include <bits/stdc++.h>

using namespace std;

int n, m, x, ans;
string g[15];
void dfs(int i, int j, int ones, int questions) {
    ones += g[i][j] == '1';
    questions += g[i][j] == '?';

    if (i == n - 1 && j == m - 1) {
        ans = max(ans, ones + min(x, questions));
        return;
    }

    if (i + 1 < n) dfs(i + 1, j, ones, questions);

    if (j + 1 < m) dfs(i, j + 1, ones, questions);
}

int main() {
    int t;

    cin >> t;

    while (t--) {
        cin >> n >> m >> x;

        for (int i = 0; i < n; i++) cin >> g[i];
        ans = 0;
        dfs(0, 0, 0, 0);
        cout << ans;
        if (t > 0) cout << '\n';
    }

    return 0;
}
代码-满分-滚动数组

\(O(nmx)\) 时间,\(O(mx+nm)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int NEG = -1000000000;
const int N = 505, M = 305;

string g[N];

int dp[N][M], cur[M];

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

    int t;

    cin >> t;

    while (t--) {
        int n, m, x;
        cin >> n >> m >> x;
        x = min(x, n + m - 1);
        fill(g, g + (n), string());
        for (int z = 0; z < n; z++) cin >> g[z];
        for (int j = 0; j <= m; j++) {
            for (int k = 0; k <= x; k++) dp[j][k] = NEG;
        }
        dp[1][0] = 0; // 第一格上方的虚拟起点。
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                for (int k = 0; k <= x; k++) cur[k] = NEG;
                for (int k = 0; k <= x; k++) {
                    int best = max(dp[j][k], dp[j - 1][k]);
                    if (best == NEG) continue;
                    cur[k] = max(cur[k], best + (g[i - 1][j - 1] == '1'));
                    if (g[i - 1][j - 1] == '?' && k < x)
                        cur[k + 1] = max(cur[k + 1], best + 1);
                }
                for (int k = 0; k <= x; k++) dp[j][k] = cur[k];
            }
        }
        int ans = 0;
        for (int k = 0; k <= x; k++) ans = max(ans, dp[m][k]);
        cout << ans;
        if (t > 0) cout << '\n';
    }

    return 0;
}

P11249 [GESP202409 七级] 小杨寻宝

原题解由陈奕涵提供,以下整理了提示与示范代码。

树上有若干宝物,可以任选起点,每条边最多走一次。
判断能否一次拿到所有宝物。
提示1-先看范围

20分做法,\(n\le 5\)。

另外20分做法,\(n\le 1000\)。

看全部数据范围,\(n\le 10^{5}\),每个输入最多 10 组。

小范围可以枚举起点,搜索走出的路径。

题目保证至少有一个宝物。

提示2-走过的路是什么形状

树上没有环。

走进一个分支后,原路返回会重复边,所以走过的只能是一条简单路径。

问题变成:所有宝物能否放在同一条路径上?

提示3-剪去无关的叶子

反复删除没有宝物的叶子。

留下的树,是连接所有宝物所必需的部分。

如果还有度数超过 2 的点,就必须走岔路,答案为 No。

否则是路径,答案为 Yes。

剪完无宝物叶子后,removed为假才是剩下的树。只要其中有一个点的度数超过2,就无法用一条路经过全部宝物:

if (!removed[u] && deg[u] > 2) ok = false;
代码-40分-枚举路径

\(O(n^{2})\) 时间,\(O(n)\) 空间。按树上每个起点搜索,也适用于 \(n\le 1000\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N];

int main() {
    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        fill(a, a + (n + 1), 0);
        int total = 0;
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
            total += a[i];
        }
        for (int z = 0; z < n + 1; z++) g[z].clear();
        for (int i = 1, u, v; i < n; i++) {
            cin >> u >> v;
            g[u].push_back(v);
            g[v].push_back(u);
        }
        bool ok = false;
        for (int s = 1; s <= n && !ok; s++) {
            // 栈内记录当前点、父亲、路径上的宝物数。
            stack<array<int, 3>> st;
            st.push({s, 0, a[s]});
            while (!st.empty()) {
                array<int, 3> state = st.top();
                int u = state[0], p = state[1], cnt = state[2];
                st.pop();
                if (cnt == total) ok = true;
                for (int v : g[u]) if (v != p) st.push({v, u, cnt + a[v]});
            }
        }
        cout << (ok ? "Yes" : "No");
        if (t > 0) cout << '\n';
    }

    return 0;
}
代码-满分-剪叶子

\(O(n)\) 时间与空间,每组数据单独处理。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N], deg[N];
bool removed[N];

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

    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        fill(a, a + (n + 1), 0);
        fill(deg, deg + (n + 1), 0);
        for (int z = 0; z < n + 1; z++) g[z].clear();
        fill(removed, removed + (n + 1), 0);
        for (int i = 1; i <= n; i++) cin >> a[i];
        for (int i = 1, u, v; i < n; i++) {
            cin >> u >> v;
            g[u].push_back(v);
            g[v].push_back(u);
            deg[u]++;
            deg[v]++;
        }
        queue<int> q;
        for (int u = 1; u <= n; u++) if (!a[u] && deg[u] <= 1) {
            removed[u] = true;
            q.push(u);
        }
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (int v : g[u]) if (!removed[v]) {
                deg[v]--;
                if (!a[v] && deg[v] <= 1) {
                    removed[v] = true;
                    q.push(v);
                }
            }
        }
        bool ok = true;
        for (int u = 1; u <= n; u++) if (!removed[u] && deg[u] > 2) ok = false;
        cout << (ok ? "Yes" : "No");
        if (t > 0) cout << '\n';
    }

    return 0;
}

P11377 [GESP202412 七级] 武器购买

每件武器最多买一次。总强度至少P,总花费最多Q。
求最少花费,买不到则输出 -1。
提示1-先看特殊性质

20分做法,\(n\le 10\),强度、费用都为 1。

另外20分做法,\(n\le 100\),费用全为 1,\(Q=2\)。

两组数据的费用都为 1:先选强度最大的武器,达到 \(P\) 就停止。

全部 \(n\le 100\),\(P,Q\le 5\times 10^{4}\)。

提示2-预算不大

总强度可能很大,预算 \(Q\) 却不超过 50000。

设 \(\operatorname{dp}_{c}\) 为恰好花 \(c\) 元,能获得的最大强度。

每件武器都可以选或不选。

提示3-每件只能用一次

费用从大到小更新,避免同一件武器重复购买。

最后从小到大找 \(\operatorname{dp}_{c}\ge P\) 的费用。

找不到就输出 -1。

读到强度p、费用cost的一件武器时,费用c倒序更新。dp[c-cost]为-1表示没有这样的购买方案:

for (int c = Q; c >= cost; c--) {
    if (dp[c - cost] == -1) continue;
    dp[c] = max(dp[c], dp[c - cost] + p);
}
代码-40分-单位费用子任务

\(O(n\log n)\) 时间,\(O(n)\) 空间。费用均为 1 时正确,覆盖前两个子任务;不适用于一般费用。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int p[N];

int main() {
    int t;

    cin >> t;

    while (t--) {
        int n, P, Q;
        cin >> n >> P >> Q;
        fill(p, p + (n), 0);

        for (int i = 0, cost; i < n; i++) cin >> p[i] >> cost;
        sort(p, p + (n));

        int sum = 0, ans = -1;
        for (int i = n - 1; i >= 0; i--) {
            sum += p[i];
            int cost = n - i;
            if (cost > Q) break;
            if (sum >= P) {
                ans = cost;
                break;
            }
        }

        cout << ans;
        if (t > 0) cout << '\n';
    }

    return 0;
}
代码-满分-01 背包

\(O(nQ)\) 时间,\(O(Q)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 50005;

int dp[N];

int main() {
    int t;

    cin >> t;

    while (t--) {
        int n, P, Q;
        cin >> n >> P >> Q;
        fill(dp, dp + (Q + 1), -1);
        dp[0] = 0;
        for (int i = 0, p, cost; i < n; i++) {
            cin >> p >> cost;
            for (int c = Q; c >= cost; c--) if (dp[c - cost] != -1)
                dp[c] = max(dp[c], dp[c - cost] + p);
        }
        int ans = -1;
        for (int c = 0; c <= Q; c++) if (dp[c] >= P) {
            ans = c;
            break;
        }
        cout << ans;
        if (t > 0) cout << '\n';
    }

    return 0;
}

P11378 [GESP202412 七级] 燃烧

在树上选一个点点火,火只能从大权值传到严格更小的相邻点。
求最多能烧到多少个点。
提示1-先看范围

20分做法,\(n\le 10\)。

另外20分做法,\(n\le 100\)。

看全部数据范围,\(n\le 10^{5}\),点权\(\le 10^{6}\)。

小范围枚举起点,每次搜索,时间 \(O(n^{2})\)。

相等权值不能传火。

提示2-从小权值开始算

设 \(f_{u}\) 为从 \(u\) 点火能烧到的点数。

\(u\) 能烧到哪些邻居?只能是权值更小的邻居,先把它们的 \(f\) 算好。

提示3-为什么可以直接相加

删掉 \(u\) 后,各个邻居属于不同分支,它们烧到的点不会重叠。

所以 \(f_u\) 等于 \(1\) 加上所有更小邻居的 \(f\) 之和。

按权值升序计算,取最大值。

order已按点权升序排好,u是当前点。先算自己,再加更小邻居的完整答案;相等时不能传火:

f[u] = 1;
for (int v : g[u]) {
    if (a[v] < a[u]) f[u] += f[v];
}
代码-40分-枚举起点

\(O(n^{2})\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
int a[N];

int main() {
    int n;

    cin >> n;

    fill(a, a + (n + 1), 0);

    for (int u = 1; u <= n; u++) cin >> a[u];

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

    int ans = 0;

    for (int s = 1; s <= n; s++) {
        stack<int> st;
        st.push(s);
        int cnt = 0;
        while (!st.empty()) {
            int u = st.top();
            st.pop();
            cnt++;
            for (int v : g[u]) if (a[v] < a[u]) st.push(v);
        }
        ans = max(ans, cnt);
    }

    cout << ans;

    return 0;
}
代码-满分-按权值排序 DP

\(O(n\log n)\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];

int a[N], order[N], f[N];

bool cmp(int u, int v) {
    return a[u] < a[v];
}

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

    int n;

    cin >> n;

    for (int u = 1; u <= n; u++) cin >> a[u];

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

    for (int i = 0; i < n; i++) order[i] = i + 1;
    sort(order, order + n, cmp);
    int ans = 1;

    for (int i = 0; i < n; i++) {
        int u = order[i];
        f[u] = 1;
        for (int v : g[u]) if (a[v] < a[u]) f[u] += f[v];
        ans = max(ans, f[u]);
    }

    cout << ans;

    return 0;
}

P11964 [GESP202503 七级] 图上移动

在无向图上,每一步沿边移动,可以重复经过点和边。
对每个起点,分别统计恰好走1到k步后能到达的点数。
提示1-先看范围

20分做法,\(k=1\)。

另外20分做法,\(n,m\le 50\)。

看全部数据范围,\(n,m\le 500\),\(k\le 20\),采用捆绑测试。

\(k=1\) 时统计不同邻居,小图可以逐个起点做布尔 DP。

提示2-恰好,不是至多

\(\operatorname{cur}_{u}\) 表示这一轮能否在 \(u\)。

下一轮把所有相邻点标记为可达。

每轮先清空新数组。

同一个点只计一次,不要用永久 visited 禁止再次到达。

提示3-同时算很多起点

用 bitset 保存一个点当前可以到达的点集。

bitset是固定长度的二进制集合,set(i)标记编号i,按位或合并集合,count()统计标记数。集合本身固定为500位,外层也使用静态数组。

走多一步的点集,是所有邻居上一轮点集的并集。

无向图中两边都要合并。

下面是基础布尔DP中的一条无向边u、v,cur记恰好走了上一轮步数的点。ndp必须每轮先清空,不能在cur上直接改:

if (cur[u]) ndp[v] = true;
if (cur[v]) ndp[u] = true;
代码-满分-基础布尔 DP

\(O(nk(n+m))\) 时间,\(O(n+m)\) 空间。\(n,m\le 500\)、\(k\le 20\) 时可处理完整范围;下面的 bitset 版本进一步加速。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

bool cur[N];
bool ndp[N];
pair<int, int> edges[N];

int main() {
    int n, m, k;

    cin >> n >> m >> k;

    for (int z = 0; z < m; z++) {
        int &u = edges[z].first, &v = edges[z].second;
        cin >> u >> v;
        u--;
        v--;
    }

    for (int s = 0; s < n; s++) {
        fill(cur, cur + (n), 0);
        fill(ndp, ndp + (n), 0);
        cur[s] = true;
        for (int step = 1; step <= k; step++) {
            fill(ndp, ndp + (n), false);
            for (int z = 0; z < m; z++) {
                int u = edges[z].first, v = edges[z].second;
                if (cur[u]) ndp[v] = true;
                if (cur[v]) ndp[u] = true;
            }
            for (int z = 0; z < n; z++) cur[z] = ndp[z];
            cout << count(cur, cur + (n), true);
            if (step < k) cout << ' ';
            else if (s + 1 < n) cout << '\n';
        }
    }

    return 0;
}
代码-满分-bitset 集合 DP

\(O\left(k(n+m)\left\lceil\frac{n}{W}\right\rceil+nk\right)\) 时间,\(O\left(n\left\lceil\frac{n}{W}\right\rceil+nk+m\right)\) 个机器字,\(W\) 为机器字长。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

pair<int, int> edges[N];

int ans[N][25];
bitset<500> cur[N], ndp[N];

int main() {
    int n, m, k;

    cin >> n >> m >> k;

    for (int z = 0; z < m; z++) {
        int &u = edges[z].first, &v = edges[z].second;
        cin >> u >> v;
        u--;
        v--;
    }

    // 每一位表示一个点是否可达,固定五百位,用静态数组保存各点的集合。

    for (int i = 0; i < n; i++) cur[i].set(i);

    for (int step = 0; step < k; step++) {
        for (int i = 0; i < n; i++) ndp[i].reset();
        for (int z = 0; z < m; z++) {
            int u = edges[z].first, v = edges[z].second;
            ndp[u] |= cur[v];
            ndp[v] |= cur[u];
        }
        for (int i = 0; i < n; i++) cur[i] = ndp[i];
        for (int i = 0; i < n; i++) ans[i][step] = cur[i].count();
    }

    for (int i = 0; i < n; i++) {
        for (int step = 0; step < k; step++) {
            cout << ans[i][step];
            if (step + 1 < k) cout << ' ';
        }
        if (i + 1 < n) cout << '\n';
    }

    return 0;
}

P11965 [GESP202503 七级] 等价消除

每次可以删除任意两个相同字符,不要求相邻。
统计能被删空的非空连续子串数量。
提示1-先看范围

20分做法,只有 \(a\)、\(b\)。

另外20分做法,长度\(\le 2000\)。

看全部数据范围,长度\(\le 2\times 10^{5}\),采用捆绑测试。

能删空,就说明每种字母的数量都是什么数?

提示2-记录字母次数的奇偶

每种字母出现偶数次,就能两两删光。

用 26 个二进制位记录奇偶。

只有 \(a\)、\(b\) 时,状态仅有 4 种。

小范围可以枚举区间并维护这个状态。

提示3-相同的前缀可以配对

两个前缀状态相同,中间子串每种字母的次数就是偶数。

用哈希表统计之前出现过多少个相同状态,先放入空前缀。

理论上有 \(2^{26}\) 种状态,但实际前缀至多 \(n+1\) 个,因此这里保留哈希表,避免开很大的静态计数数组。

答案用 long long。

mask初始为0,cnt[0]先放一个空前缀。当前字母改变它对应位的奇偶,计数顺序与区间乘积相同:

mask ^= 1 << (c - 'a');
ans += cnt[mask];
cnt[mask]++;
代码-20分-长度子任务

\(O(n^{2})\) 时间,\(O(n)\) 空间。仅用于长度\(\le 2000\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    string s;

    cin >> n >> s;

    long long ans = 0;

    for (int l = 0; l < n; l++) {
        int mask = 0;
        for (int r = l; r < n; r++) {
            mask ^= 1 << (s[r] - 'a');
            ans += mask == 0;
        }
    }

    cout << ans;

    return 0;
}
代码-满分-前缀状态计数

哈希表期望 \(O(n)\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

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

    int n;
    string s;

    cin >> n >> s;

    unordered_map<int, long long> cnt;
    cnt.reserve(2 * n + 1);
    cnt[0] = 1;
    int mask = 0;
    long long ans = 0;

    for (char c : s) {
        mask ^= 1 << (c - 'a');
        ans += cnt[mask];
        cnt[mask]++;
    }

    cout << ans;

    return 0;
}

P13017 [GESP202506 七级] 线图

原图的每条边变成一个点。原图中两条边共用端点,线图中对应两点就连边。
求线图的边数,原图没有重边和自环。
提示1-先看范围

60分做法,\(n,m\le 500\)。

看全部数据范围,\(n,m\le 10^{5}\)。

小范围可以枚举两条边,检查是否有公共端点。

提示2-围着一个点数

原图中一个点连接了 \(d\) 条边。

任意选两条,都会在线图中形成一条边。

这个点贡献 \(\frac{d(d-1)}{2}\)。

把每个点的贡献相加。

提示3-会不会重复

没有重边,两条不同边最多共用一个端点,所以不会重复统计。

答案可能超过 int,用 long long,乘法前也要转成 long long。

读边时,两端度数都加一。处理完输入后,对每个点u累加它能配成的边对;乘法先提升到long long:

ans += 1LL * deg[u] * (deg[u] - 1) / 2;
代码-60分-枚举边对

\(O(n+m^{2})\) 时间,\(O(m)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

pair<int, int> e[N];

int main() {
    int n, m;

    cin >> n >> m;

    for (int z = 0; z < m; z++) cin >> e[z].first >> e[z].second;

    long long ans = 0;

    for (int i = 0; i < m; i++) for (int j = i + 1; j < m; j++) {
        int u = e[i].first, v = e[i].second;
        int x = e[j].first, y = e[j].second;
        ans += u == x || u == y || v == x || v == y;
    }

    cout << ans;

    return 0;
}
代码-满分-统计度数

\(O(n+m)\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int deg[N];

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

    int n, m;

    cin >> n >> m;

    fill(deg, deg + (n + 1), 0);

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        deg[u]++;
        deg[v]++;
    }

    long long ans = 0;

    for (int u = 1; u <= n; u++) ans += 1LL * deg[u] * (deg[u] - 1) / 2;

    cout << ans;

    return 0;
}

P13018 [GESP202506 七级] 调味平衡

每种食材最多选一次。酸度与甜度分别求和,要求两者相等。
求酸度与甜度之和的最大值,可以什么都不选。
提示1-先看范围

40分做法,\(n\le 10\),\(a_{i},b_{i}\le 10\)。

另外20分做法,\(n\le 50\),\(a_{i},b_{i}\le 10\)。

看全部数据范围,\(n\le 100\),\(a_{i},b_{i}\le 500\)。

\(n\le 10\) 可以枚举子集。

后面的范围要把选法合并。

提示2-只关心酸甜差

选一份食材,酸甜差增加 \(a_{i}-b_{i}\),总味道增加 \(a_{i}+b_{i}\)。

设 \(\operatorname{dp}_{d}\) 为酸甜差为 \(d\) 时的最大总味道,最后取 \(\operatorname{dp}_{0}\)。

提示3-差值也可能是负数

所有差值的绝对值之和最多 50000,用偏移量存负下标。

每件食材用新数组更新一次,别重复选。

只有 \(\operatorname{dp}_{0}=0\),其他状态初始不可达。

处理第i份食材时,先把dp复制到ndp表示不选,再从旧dp转移。D是偏移量,d和nd分别为原差值和新差值:

int nd = d + a[i] - b[i];
if (-D <= nd && nd <= D)
    ndp[nd + D] = max(ndp[nd + D], dp[d + D] + a[i] + b[i]);
代码-40分-枚举子集

\(O(n2^{n})\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int a[N], b[N];

int main() {
    int n;

    cin >> n;

    fill(a, a + (n), 0);
    fill(b, b + (n), 0);

    for (int i = 0; i < n; i++) cin >> a[i] >> b[i];

    int ans = 0;

    for (int mask = 0; mask < (1 << n); mask++) {
        int A = 0, B = 0;
        for (int i = 0; i < n; i++) if (mask >> i & 1) {
            A += a[i];
            B += b[i];
        }
        if (A == B) ans = max(ans, A + B);
    }

    cout << ans;

    return 0;
}
代码-满分-差值背包

令 \(D=\sum_{i=1}^{n}|a_i-b_i|\),时间 \(O(n(D+1))\),空间 \(O(D+1)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;
const int NEG = -1000000000;

int a[N], b[N];
int dp[100005];
int ndp[100005];

int main() {
    int n;

    cin >> n;

    fill(a, a + (n), 0);
    fill(b, b + (n), 0);
    int D = 0;

    for (int i = 0; i < n; i++) {
        cin >> a[i] >> b[i];
        D += abs(a[i] - b[i]);
    }

    fill(dp, dp + (2 * D + 1), NEG);
    dp[D] = 0;

    for (int i = 0; i < n; i++) {
        for (int z = 0; z <= 2 * D; z++) ndp[z] = dp[z];
        int delta = a[i] - b[i];
        for (int d = -D; d <= D; d++) {
            if (dp[d + D] == NEG) continue;
            int nd = d + delta;
            if (-D <= nd && nd <= D)
                ndp[nd + D] = max(ndp[nd + D], dp[d + D] + a[i] + b[i]);
        }
        for (int z = 0; z < 2 * D + 1; z++) dp[z] = ndp[z];
    }

    cout << dp[D];

    return 0;
}

P14077 [GESP202509 七级] 连通图

给定无向图,允许重边和自环。
求至少加几条边,才能让所有点连通。
提示1-先看范围

40分做法,\(n,m\le 100\)。

看全部数据范围,\(n,m\le 10^{5}\)。

先数清楚原图分成了几块。

孤立点也算一块。

提示2-一条边能连接几块

新增一条边,最多让块数减少 1。

若有 \(c\) 个连通块,至少要 \(c-1\) 条边。

任选一块,连向其他各块,就能做到。

提示3-怎样数块

小范围用邻接矩阵 BFS。

大范围用邻接表或并查集。

自环不会连接两块,重复边也不会让答案多减一次。

并查集开始每点独立,blocks为n。把边的两端先找代表;只有代表不同,连通块数才减少一次:

u = find(u);
v = find(v);
if (u != v) {
    p[v] = u;
    blocks--;
}
代码-40分-邻接矩阵 BFS

\(O(n^{2}+m)\) 时间,\(O(n^{2})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

bool vis[100005];

bool g[N][N];

int main() {
    int n, m;

    cin >> n >> m;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u][v] = g[v][u] = true;
    }

    fill(vis, vis + (n + 1), 0);
    int blocks = 0;

    for (int s = 1; s <= n; s++) if (!vis[s]) {
        blocks++;
        queue<int> q;
        q.push(s);
        vis[s] = true;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (int v = 1; v <= n; v++) if (g[u][v] && !vis[v]) {
                vis[v] = true;
                q.push(v);
            }
        }
    }

    cout << blocks - 1;

    return 0;
}
代码-满分-并查集

\(O(n+m\alpha(n))\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int p[N], sz[N];

int find(int x) {
    while (p[x] != x) {
        p[x] = p[p[x]];
        x = p[x];
    }

    return x;
}

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

    int n, m;

    cin >> n >> m;

    for (int i = 0; i <= n; i++) {
        p[i] = i;
        sz[i] = 1;
    }
    int blocks = n;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        u = find(u);
        v = find(v);
        if (u == v) continue;
        if (sz[u] < sz[v]) swap(u, v);
        p[v] = u;
        sz[u] += sz[v];
        blocks--;
    }

    cout << blocks - 1;

    return 0;
}

P14078 [GESP202509 七级] 金币收集

从时刻0、位置0出发,每秒只能停留或向右走一格。
金币只在指定时刻、指定位置能收集,求最多收集数量。
提示1-先看范围

40分做法,\(n\le 8\)。

另外30分做法,\(n,x_{i},t_{i}\le 100\)。

看全部数据范围,\(n\le 10^{5}\),\(x_{i},t_{i}\le 10^{9}\)。

先按时间排序,小范围枚举前一枚收集的金币,做 \(O(n^{2})\) DP,可覆盖前70分。

提示2-两枚金币能接起来吗

从 \((x_1,t_1)\) 到 \((x_2,t_2)\),需要 \(x_2\ge x_1\),且 \(x_2-x_1\le t_2-t_1\)。

把后一个条件移项,变成 \(t_2-x_2\ge t_1-x_1\)。

提示3-变成两个数不下降

令 \(y=t-x\)。

先删掉 \(y<0\) 的金币,因为从起点就赶不到。

按 \(x\) 升序、再按 \(y\) 升序排序,求 \(y\) 的最长不下降子序列。

用 upper_bound,允许相等。

同时同地的多枚金币可以都拿。

tail[len-1]是当前最长长度对应的最小结尾,len初始为0。已按x、y升序后,处理一枚金币的y时找第一个严格大于它的位置:

int pos = upper_bound(tail, tail + len, y) - tail;
tail[pos] = y;
if (pos == len) len++;
代码-70分-金币 DP

\(O(n^{2})\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

pair<long long, long long> coins[N];

int dp[N];

int main() {
    int n;

    cin >> n;

    int cnt = 0; // coins保存时间和位置。

    for (int i = 0; i < n; i++) {
        long long x, t;
        cin >> x >> t;
        if (x <= t) coins[cnt++] = {t, x};
    }

    sort(coins, coins + cnt);
    int sz = cnt, ans = 0;
    fill(dp, dp + (sz), 1);

    for (int i = 0; i < sz; i++) {
        long long t = coins[i].first, x = coins[i].second;
        for (int j = 0; j < i; j++) {
            long long pt = coins[j].first, px = coins[j].second;
            if (x >= px && x - px <= t - pt) dp[i] = max(dp[i], dp[j] + 1);
        }
        ans = max(ans, dp[i]);
    }

    cout << ans;

    return 0;
}
代码-满分-最长不下降子序列

\(O(n\log n)\) 时间,\(O(n)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

pair<long long, long long> coins[N];
long long tail[N];

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

    int n;

    cin >> n;

    int cnt = 0;

    for (int i = 0; i < n; i++) {
        long long x, t;
        cin >> x >> t;
        if (t >= x) coins[cnt++] = {x, t - x};
    }

    sort(coins, coins + cnt);
    int len = 0;

    for (int i = 0; i < cnt; i++) {
        long long y = coins[i].second;
        int pos = upper_bound(tail, tail + len, y) - tail; // 找第一个大于y的位置,允许相等。
        tail[pos] = y;
        if (pos == len) len++;
    }

    cout << len;

    return 0;
}

P14921 [GESP202512 七级] 城市规划

城市建设难度,是到其他城市的最短路长度的最大值。
求难度最小的城市;并列时输出最小编号,图保证连通。
提示1-先看范围

40分做法,\(n\le 300\)。

看全部数据范围,\(n,m\le 2000\)。

Floyd 的 \(O(n^{3})\) 适合小范围。

大图边很少,而且每条边长度都是 1。

提示2-每个城市都试一次

从一个城市 BFS,就能求出它到其他城市的最短距离。

其中最大的距离,就是这个城市的难度。

提示3-处理并列

按编号从小到大枚举起点。

只有难度严格变小时才更新答案。

总时间 \(O(n(n+m))\),这里约是千万级操作,能处理完整范围。

从候选城市s做完BFS后,最大距离就是score。按编号升序试s,只有严格变小才更新,才能保留并列中的最小编号:

if (score < best) {
    best = score;
    ans = s;
}
代码-40分-Floyd

\(O(n^{3}+m)\) 时间,\(O(n^{2})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int INF = 1000000;
const int N = 305;

int d[N][N];

int main() {
    int n, m;

    cin >> n >> m;

    for (int u = 1; u <= n; u++) {
        for (int v = 1; v <= n; v++) d[u][v] = INF;
    }

    for (int u = 1; u <= n; u++) d[u][u] = 0;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        d[u][v] = min(d[u][v], 1);
        d[v][u] = min(d[v][u], 1);
    }

    for (int k = 1; k <= n; k++) for (int u = 1; u <= n; u++)
        for (int v = 1; v <= n; v++) d[u][v] = min(d[u][v], d[u][k] + d[k][v]);

    int best = INF, ans = 1;

    for (int u = 1; u <= n; u++) {
        int score = 0;
        for (int v = 1; v <= n; v++) score = max(score, d[u][v]);
        if (score < best) {
            best = score;
            ans = u;
        }
    }

    cout << ans;

    return 0;
}
代码-满分-逐点 BFS

\(O(n(n+m))\) 时间,\(O(n+m)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 2005;

vector<int> g[N];
int d[N];

int main() {
    int n, m;

    cin >> n >> m;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    int best = n + 1, ans = 1;

    for (int s = 1; s <= n; s++) {
        fill(d, d + (n + 1), -1);
        queue<int> q;
        d[s] = 0;
        q.push(s);
        int score = 0;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            score = max(score, d[u]);
            for (int v : g[u]) if (d[v] == -1) {
                d[v] = d[u] + 1;
                q.push(v);
            }
        }
        if (score < best) {
            best = score;
            ans = s;
        }
    }

    cout << ans;

    return 0;
}

P14922 [GESP202512 七级] 学习小组

把所有同学分成若干小组。k人小组得分为a[k]+组内最大c-组内最小c。
求所有小组得分之和的最大值。
提示1-先看特殊性质

40分做法,所有 \(c_{i}=0\)。

看全部数据范围,\(n\le 300\),\(a_{i},c_{i}\le 10^{4}\)。

\(c\) 全为 0 时,只需考虑每组人数。

设 \(f_{i}\) 为分完 \(i\) 人的最高分,枚举最后一组人数即可。

提示2-哪些组能贡献差值

单人组的最大值和最小值相同,差值为 0。

假设有 \(q\) 个至少两人的小组,最多能得到多少差值之和?

提示3-让两头的人配成组

把 \(c\) 排序。

用最大的 \(q\) 人当各组最大值,最小的 \(q\) 人当各组最小值。

其他人填进这些组或单独成组,不会改变差值。

差值之和就是最大的 \(q\) 个 \(c\) 之和减去最小的 \(q\) 个 \(c\) 之和。

提示4-还要决定每组人数

设 \(\operatorname{dp}_{i,q}\) 为分完 \(i\) 人、其中 \(q\) 组至少有两人时,基础分的最大值。

枚举最后一组人数 \(k\),加 \(a_{k}\)。

\(k\ge 2\) 时 \(q\) 加一。

最后加上 \(q\) 对极值的差值,取最大值。

正在算i人、q个多人组,枚举最后一组人数k。若k至少为2,前面就少一个多人组;前态必须可达:

int last = q - (k >= 2);
if (last < 0 || last > (i - k) / 2) continue;
if (dp[i - k][last] != NEG)
    dp[i][q] = max(dp[i][q], dp[i - k][last] + a[k]);
代码-40分-只算基础分

\(O(n^{2})\) 时间,\(O(n)\) 空间。仅适用于所有 \(c_{i}=0\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 305;

int c[N], a[N], dp[N];

int main() {
    int n;

    cin >> n;

    fill(c, c + (n), 0);
    fill(a, a + (n + 1), 0);
    fill(dp, dp + (n + 1), 0);

    for (int i = 0; i < n; i++) cin >> c[i];

    for (int k = 1; k <= n; k++) cin >> a[k];

    for (int i = 1; i <= n; i++)
        for (int k = 1; k <= i; k++) dp[i] = max(dp[i], dp[i - k] + a[k]);

    cout << dp[n];

    return 0;
}
代码-满分-人数与组数 DP

\(O(n^{3})\) 时间,\(O(n^{2})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const long long NEG = -(1LL << 60);
const int N = 305;

int c[N], a[N];

long long dp[N][N];

int main() {
    int n;

    cin >> n;

    fill(c, c + (n), 0);
    fill(a, a + (n + 1), 0);

    for (int i = 0; i < n; i++) cin >> c[i];

    for (int k = 1; k <= n; k++) cin >> a[k];
    sort(c, c + (n));
    for (int i = 0; i <= n; i++) {
        for (int q = 0; q <= n / 2; q++) dp[i][q] = NEG;
    }
    dp[0][0] = 0;

    for (int i = 1; i <= n; i++) {
        for (int q = 0; q <= i / 2; q++) {
            for (int k = 1; k <= i; k++) {
                int last = q - (k >= 2);
                if (last < 0 || last > (i - k) / 2) continue;
                if (dp[i - k][last] != NEG)
                    dp[i][q] = max(dp[i][q], dp[i - k][last] + a[k]);
            }
        }
    }

    long long ans = dp[n][0], bonus = 0;

    for (int q = 1; q <= n / 2; q++) {
        bonus += c[n - q] - c[q - 1];
        ans = max(ans, dp[n][q] + bonus);
    }

    cout << ans;

    return 0;
}

P15802 [GESP202603 七级] 拆分

把n写成若干正整数之和,允许不拆。
求最大乘积对10^9取模后的结果。
提示1-先看范围

40分做法,\(n\le 50\)。

看全部数据范围,\(n\le 10^{6}\),最多 \(10^{4}\) 组。

小范围用 DP:枚举拆出的第一份 \(k\),乘上剩下部分的最佳乘积。

先比较真实乘积,最后才取模。

提示2-大数拆小会更好吗

一份 \(k\ge 5\) 时,拆成 3 和 \(k-3\),乘积会变大。

1 通常不划算,4 可以拆成 \(2+2\)。

所以重点考虑 2 和 3。

三个 2 不如两个 3。

提示3-尽量用 3,处理余数

\(n\le 3\) 时不拆。

其他情况尽量拆成 3。

余 1 时,把最后的 \(3+1\) 改成 \(2+2\)。

余 2 时保留一个 2。

用快速幂计算取模结果。

余1时不能留一个单独的1,我把最后一个3和这个1合成4。这里power是按模快速幂:

if (n % 3 == 1) cout << power(3, n / 3 - 1) * 4 % MOD;
代码-40分-乘积 DP

预处理 \(O(50^{2})\),每组 \(O(1)\);空间 \(O(50)\)。\(n\le 50\) 的真实最大乘积能放进 long long。

#include <bits/stdc++.h>

using namespace std;

const long long MOD = 1000000000LL;

long long dp[55];

int main() {
    dp[0] = 1;

    for (int n = 1; n <= 50; n++)
        for (int k = 1; k <= n; k++) dp[n] = max(dp[n], k * dp[n - k]);

    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        cout << dp[n] % MOD;
        if (t > 0) cout << '\n';
    }

    return 0;
}
代码-满分-贪心与快速幂

每组 \(O(\log n)\) 时间,\(O(1)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const long long MOD = 1000000000LL;

long long power(long long a, int b) {
    long long result = 1;

    while (b) {
        if (b & 1) result = result * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }

    return result;
}

int main() {
    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        if (n <= 3) {
            cout << n;
            if (t > 0) cout << '\n';
            continue;
        }
        if (n % 3 == 0) cout << power(3, n / 3);
        else if (n % 3 == 1) cout << power(3, n / 3 - 1) * 4 % MOD;
        else cout << power(3, n / 3) * 2 % MOD;

        if (t > 0) cout << '\n';
    }

    return 0;
}

P15803 [GESP202603 七级] 物流网络

从城市1运货到城市n。路径上景观评分最高的一条路免费,若并列只免一条。
求最少运输费用,不可达输出 -1。
提示1-先看范围

\(n,m\le 5000\),费用和景观评分\(\le 10^{9}\)。

题目没有给出部分分分组。

不是“随便免一条路”:免的路必须拥有整条路径的最高景观评分。

费用总和用 long long。

提示2-先选免费道路

假设免费的是 \((u,v)\),景观评分为 \(b\)。

其他道路的评分都必须\(\le b\)。

只用这些道路,求 1 到 \(u\)、\(v\) 到 \(n\) 的最小费用,两段相加。

反方向也试一次。

提示3-按评分逐步加边

把道路按 \(b\) 从小到大加入图。

分别维护从 1、从 \(n\) 出发的最短路。

新增道路后,只把距离变小的端点放入堆,再继续 Dijkstra,不用每次从头算。

提示4-同分道路一起处理

评分相同的道路先全部加入,更新完两份距离,再逐条尝试免费。

此时图里没有评分更高的道路,免费条件就满足了。

评分相同的道路全部加入并更新两份距离后,再试其中一条边u、v免费。费用w不再加入,且两段都必须可达:

if (d1[u] != INF && dn[v] != INF) ans = min(ans, d1[u] + dn[v]);
if (d1[v] != INF && dn[u] != INF) ans = min(ans, d1[v] + dn[u]);
代码-满分-增量最短路

保守时间界 \(O(m(n+m)\log(n+m))\),空间 \(O(n+m)\)。通过复用距离减少重复搜索;没有另设部分分。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<ll, int> State;

const int N = 5005;
const ll INF = 1LL << 60;

struct Edge {
    int u, v;
    ll w, b;
} e[N];

vector<pair<int, ll> > g[N];
ll d1[N], dn[N];

bool cmp(Edge x, Edge y) {
    return x.b < y.b;
}

void relax(int v, ll value, ll d[], priority_queue<State, vector<State>, greater<State> > &q) {
    if (value < d[v]) {
        d[v] = value;
        q.push(make_pair(value, v));
    }
}

void update(ll d[], priority_queue<State, vector<State>, greater<State> > &q) {
    while (!q.empty()) {
        ll distance = q.top().first;
        int u = q.top().second;
        q.pop();
        if (distance != d[u]) continue;

        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i].first;
            ll w = g[u][i].second;
            relax(v, distance + w, d, q);
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; i++) cin >> e[i].u >> e[i].v >> e[i].w >> e[i].b;

    if (n == 1) {
        cout << 0;

        return 0;
    }

    sort(e, e + m, cmp);
    for (int i = 1; i <= n; i++) d1[i] = dn[i] = INF;
    d1[1] = 0;
    dn[n] = 0;
    ll ans = INF;

    for (int l = 0; l < m; ) {
        int r = l;
        while (r < m && e[r].b == e[l].b) r++;
        // 小根堆保存新变短的距离;堆内元素数会变化,因此保留priority_queue。
        priority_queue<State, vector<State>, greater<State> > q1, qn;
        for (int i = l; i < r; i++) {
            int u = e[i].u, v = e[i].v;
            ll w = e[i].w;
            g[u].push_back(make_pair(v, w));
            g[v].push_back(make_pair(u, w));
            if (d1[u] != INF) relax(v, d1[u] + w, d1, q1);
            if (d1[v] != INF) relax(u, d1[v] + w, d1, q1);
            if (dn[u] != INF) relax(v, dn[u] + w, dn, qn);
            if (dn[v] != INF) relax(u, dn[v] + w, dn, qn);
        }
        update(d1, q1);
        update(dn, qn);

        for (int i = l; i < r; i++) {
            int u = e[i].u, v = e[i].v;
            if (d1[u] != INF && dn[v] != INF) ans = min(ans, d1[u] + dn[v]);
            if (d1[v] != INF && dn[u] != INF) ans = min(ans, d1[v] + dn[u]);
        }
        l = r;
    }

    cout << (ans == INF ? -1 : ans);

    return 0;
}

P17014 [GESP202606 七级] 染色

无向图没有重边、自环,每个点的度数都是2。
给点染色,要求相邻点颜色不同,求最少颜色数。
提示1-先看特殊性质

40分做法,各组 \(n\) 之和\(\le 500\)。

看全部数据范围,最多 100 组,\(n\) 之和\(\le 10^{5}\)。

关键不是小范围,而是每个点度数恰好为 2:每个连通块都是一个环。

提示2-沿环交替染色

偶数环可以用两种颜色交替染。

奇数环回到起点时会冲突,需要第三种颜色。

只要有一个奇数环,答案就是 3。

否则答案是 2。

提示3-数每个环的长度

BFS 找出每个连通块,数点数即可。

注意多组数据,每组重新建图。

BFS找出一个连通块后,cnt是这个环的点数。只要发现奇环,就必须用第三种颜色:

if (cnt % 2 == 1) ans = 3;
代码-40分-邻接矩阵染色

\(O(n^{2})\) 时间与空间。小范围直接尝试两种颜色。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

int color[100005];

bool g[N][N];

int main() {
    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        memset(g, 0, sizeof g);

        for (int i = 0, u, v; i < n; i++) {
            cin >> u >> v;
            g[u][v] = g[v][u] = true;
        }
        fill(color, color + (n + 1), -1);
        bool ok = true;
        for (int s = 1; s <= n; s++) if (color[s] == -1) {
            queue<int> q;
            q.push(s);
            color[s] = 0;
            while (!q.empty()) {
                int u = q.front();
                q.pop();
                for (int v = 1; v <= n; v++) if (g[u][v]) {
                    if (color[v] == -1) {
                        color[v] = color[u] ^ 1;
                        q.push(v);
                    }
                    else if (color[v] == color[u]) ok = false;
                }
            }
        }
        cout << (ok ? 2 : 3);
        if (t > 0) cout << '\n';
    }

    return 0;
}
代码-满分-统计环长

每组 \(O(n)\) 时间与空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

vector<int> g[N];
bool vis[N];

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

    int t;

    cin >> t;

    while (t--) {
        int n;
        cin >> n;
        for (int z = 0; z < n + 1; z++) g[z].clear();
        fill(vis, vis + (n + 1), 0);
        for (int i = 0, u, v; i < n; i++) {
            cin >> u >> v;
            g[u].push_back(v);
            g[v].push_back(u);
        }
        int ans = 2;
        for (int s = 1; s <= n; s++) if (!vis[s]) {
            queue<int> q;
            q.push(s);
            vis[s] = true;
            int cnt = 0;
            while (!q.empty()) {
                int u = q.front();
                q.pop();
                cnt++;
                for (int v : g[u]) if (!vis[v]) {
                    vis[v] = true;
                    q.push(v);
                }
            }
            if (cnt % 2) ans = 3;
        }
        cout << ans;
        if (t > 0) cout << '\n';
    }

    return 0;
}

P17015 [GESP202606 七级] 消消乐

每次删除一个数,得分为当时它左右邻居之和,没有邻居的一侧算0。
删完所有数,求最高总分。
提示1-先看范围

40分做法,\(n\le 50\)、\(a_{i}\le 1000\)。

看全部数据范围,\(n\le 100\)、\(a_{i}\le 10^{9}\)。

枚举删除顺序有 \(n!\) 种,\(n=50\) 也做不了。

\(O(n^{3})\) 的区间 DP 已能处理全部范围,得分用 long long。

提示2-先删谁不好想,最后删谁呢

设位置 \(k\) 是区间 \([l,r]\) 中最后删除的数。

删 \(k\) 前,左半段、右半段都删空了。

此时 \(k\) 的邻居一定是区间外的 \(a_{l-1}\)、\(a_{r+1}\)。

提示3-左右分别算

设 \(f_{l,r}\) 为保留区间外两侧邻居时,删空 \([l,r]\) 的最高分。

枚举最后删的位置 \(k\),取最大值:

\[ f_{l,r}=\max_{l\le k\le r}\left(f_{l,k-1}+f_{k+1,r}+a_{l-1}+a_{r+1}\right) \]

空区间为 \(0\),补 \(a_0=a_{n+1}=0\)。

按区间长度递增处理[l,r],k枚举最后删的位置。左右已删空,当前只取区间外两侧的邻居:

f[l][r] = max(f[l][r], f[l][k - 1] + f[k + 1][r]
                       + a[l - 1] + a[r + 1]);
代码-满分-区间 DP

\(O(n^{3})\) 时间,\(O(n^{2})\) 空间。小范围与完整范围使用同一种算法。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

long long a[N], f[N][N];

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

    int n;

    cin >> n;

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

    for (int len = 1; len <= n; len++) for (int l = 1; l + len - 1 <= n; l++) {
        int r = l + len - 1;
        for (int k = l; k <= r; k++)
            f[l][r] = max(f[l][r], f[l][k - 1] + f[k + 1][r] + a[l - 1] + a[r + 1]);
    }

    cout << f[1][n];

    return 0;
}

P17459 [GESP202609 七级] 必经之路

有向图中,原图入度为0的点是起点,出度为0的点是终点。
求所有从合法起点到合法终点的路径都会经过的点,按编号升序输出。
提示1-先看范围

40分做法,\(n\le 100\)、\(m\le 200\)。

看全部数据范围,\(n\le 1000\)、\(m\le 2000\)。

这个范围允许逐个点检查。

图可能有环,不能直接做拓扑 DP。

提示2-假装删掉这个点

如果删去 \(u\) 后,仍能从某个合法起点走到某个合法终点,就有一条路绕过 \(u\)。

如果所有这样的路都断了,\(u\) 就是必经点。

提示3-起终点按原图确定

从原图所有合法起点一起 BFS,跳过被删除的点。

原图终点仍可达就不必经。

不要删点后重新计算起终点,否则会凭空增加新路径。

BFS只能从原图起点出发。ban是这次假装删除的点,原来终点只要还有一个可达,就不是必经点:

for (int v : g[u]) {
    if (v == ban || vis[v]) continue;
    vis[v] = true;
    q.push(v);
}
代码-40分-邻接矩阵搜索

\(O(n^{3}+m)\) 时间,\(O(n^{2})\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int in[1005], out[1005], ans[1005];

bool vis[1005];

bool g[N][N];

int main() {
    int n, m;

    cin >> n >> m;

    int cnt = 0;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u][v] = true;
        in[v]++;
        out[u]++;
    }

    for (int ban = 1; ban <= n; ban++) {
        queue<int> q;
        fill(vis, vis + (n + 1), 0);
        for (int s = 1; s <= n; s++) if (!in[s] && s != ban) {
            vis[s] = true;
            q.push(s);
        }
        bool reachable = false;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            if (!out[u]) {
                reachable = true;
                break;
            }
            for (int v = 1; v <= n; v++) if (g[u][v] && v != ban && !vis[v]) {
                vis[v] = true;
                q.push(v);
            }
        }
        if (!reachable) ans[cnt++] = ban;
    }

    cout << cnt;
    if (cnt > 0) cout << '\n';

    for (int i = 0; i < (int)cnt; i++) {
        if (i > 0) cout << ' ';
        cout << ans[i];
    }

    return 0;
}
代码-满分-删点与多源 BFS

\(O(n(n+m))\) 时间,\(O(n+m)\) 空间。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int in[N], out[N], ans[N];

vector<int> g[N];
bool vis[N];

int main() {
    int n, m;

    cin >> n >> m;

    int cnt = 0;

    for (int i = 0, u, v; i < m; i++) {
        cin >> u >> v;
        g[u].push_back(v);
        in[v]++;
        out[u]++;
    }

    for (int ban = 1; ban <= n; ban++) {
        fill(vis, vis + (n + 1), 0);
        queue<int> q;
        for (int s = 1; s <= n; s++) if (!in[s] && s != ban) {
            vis[s] = true;
            q.push(s);
        }
        bool reachable = false;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            if (!out[u]) {
                reachable = true;
                break;
            }
            for (int v : g[u]) if (v != ban && !vis[v]) {
                vis[v] = true;
                q.push(v);
            }
        }
        if (!reachable) ans[cnt++] = ban;
    }

    cout << cnt;
    if (cnt > 0) cout << '\n';

    for (int i = 0; i < (int)cnt; i++) {
        if (i > 0) cout << ' ';
        cout << ans[i];
    }

    return 0;
}

P17460 [GESP202609 七级] 括号序列

给定一个长度为n的括号字符串,可以删除任意多个字符,保留字符的顺序。
求有多少种选取下标的方式,能够得到合法括号序列。
即使得到的字符串相同,只要选取的下标不同,就分别计数。
空子序列也是合法括号序列,答案对10^9取模。
提示1-先看数据范围

40分做法,\(n\le400\)。

看全部数据范围,\(n\le2000\)。

没有其他特殊性质分组。

每个字符都可以选或不选,共有 \(2^{n}\) 种选法,直接枚举太慢。

先想一个 \(O(n^{3})\) 的方法拿部分分,再尝试优化到 \(O(n^{2})\)。

提示2-部分分:找到配对的右括号

一个非空合法序列,可以拆成 (A)B,其中 \(A\)、\(B\) 也都合法。

如果选了第一个左括号,能不能枚举与它配对的右括号,再分别统计里面和后面的选法?

设 \(f_{l,r}\) 为区间 \([l,r]\) 中的合法子序列数量,空区间记为 \(1\)。

  • 不选位置 \(l\):有 \(f_{l+1,r}\) 种。
  • 选位置 \(l\):它必须是 (。枚举配对的 ) 的位置 \(k\),贡献为 \(f_{l+1,k-1}\times f_{k+1,r}\)。

把这些方案相加即可。

两个区间端点加上一个配对位置,时间复杂度为 \(O(n^{3})\)。

提示3-满分:还差几个右括号

从左往右读一个合法序列,右括号不能比左括号多,最后两者数量要相等。

假设已经选了几个字符,还需要知道整个字符串吗?

只需知道:有几个左括号还没配对。

记这个数量为 \(j\)。

设 \(\operatorname{dp}_{i,j}\) 为处理前 \(i\) 个字符后,还有 \(j\) 个左括号没配对的选法数量。

过程中不允许出现多余的右括号。

提示4-当前字符选不选

每个字符都有两种选择:

  • 不选:\(j\) 不变。
  • 选 (:\(j\) 加一。
  • 选 ):\(j\) 减一,但必须先有 \(j>0\)。

开始时 \(\operatorname{dp}_{0,0}=1\),表示什么都不选。

最后取 \(\operatorname{dp}_{n,0}\):所有左括号都配好了,空子序列也已经算在里面。

\(i\)、\(j\) 都最多到 \(n\),时间复杂度为 \(O(n^{2})\)。

只保存前后两层,空间复杂度为 \(O(n)\)。

提示5-别漏算,也别多算

(() 的两个左括号,都能与最后的右括号组成 ()。

选的下标不同,要算两种。

再加空子序列,答案为 \(3\)。

全是左括号、全是右括号,或者右括号全在左括号前面,都只有空子序列,答案为 \(1\)。

每次计算都对 \(10^{9}\) 取模。

部分分代码有乘法,要用 long long。

每轮先清空ndp。旧状态j表示还未配对的左括号数;以下示范不选,以及选一个右括号,选左括号的分支见完整代码:

add(ndp[j], dp[j]);
if (s[i] == ')' && j > 0) add(ndp[j - 1], dp[j]);
代码-40分-区间动态规划

适用于 \(n\le400\)。时间复杂度 \(O(n^{3})\),空间复杂度 \(O(n^{2})\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 405;
const long long MOD = 1000000000LL;

long long f[N][N];

int main() {
    int n;
    string s;

    cin >> n >> s;
    s = " " + s;

    // 空区间只有空子序列这一种方案。

    for (int l = 1; l <= n + 1; l++) {
        f[l][l - 1] = 1;
    }

    for (int len = 1; len <= n; len++) {
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            f[l][r] = f[l + 1][r]; // 不选位置l。

            if (s[l] != '(') continue;
            for (int k = l + 1; k <= r; k++) {
                if (s[k] == ')') {
                    long long ways = f[l + 1][k - 1] * f[k + 1][r] % MOD;
                    f[l][r] = (f[l][r] + ways) % MOD;
                }
            }
        }
    }

    cout << f[1][n];

    return 0;
}
代码-满分-余额动态规划与滚动数组

适用于 \(n\le2000\)。时间复杂度 \(O(n^{2})\),空间复杂度 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 2005;
const int MOD = 1000000000;

int dp[N], ndp[N];

void add(int &a, int b) {
    a += b;

    if (a >= MOD) a -= MOD;
}

int main() {
    int n;
    string s;

    cin >> n >> s;

    dp[0] = 1;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= n; j++) ndp[j] = 0;
        for (int j = 0; j <= i; j++) {
            add(ndp[j], dp[j]); // 不选当前字符。
            if (s[i] == '(') {
                add(ndp[j + 1], dp[j]);
            }
            else if (j > 0) {
                add(ndp[j - 1], dp[j]);
            }
        }
        for (int j = 0; j <= n; j++) dp[j] = ndp[j];
    }

    cout << dp[0];

    return 0;
}