Codeforces Round 1086 (Div. 2) 题解

笑点解析:打到一半睡着了(还好是 unrated)。

比赛网址:Dashboard - Codeforces Round 1086 (Div. 2) - Codeforces

A - Bingo Candies

给定一个 n × n n \times n n×n 的网格,其中每个格子包含一个彩色糖果。判断是否可以将这些糖果重新排列,使得网格中不存在任何一行或任何一列的所有 n n n 个糖果颜色均相同。

数据范围: 1 ≤ t ≤ 500 1 \le t \le 500 1≤t≤500, 1 ≤ n ≤ 100 1 \le n \le 100 1≤n≤100, ∑ n ≤ 500 \sum n \le 500 ∑n≤500, 1 ≤ a i , j ≤ n 2 1 \le a_{i,j} \le n^2 1≤ai,j≤n2

不难发现不合法当且仅当某个数的出现次数大于等于 n ( n − 1 ) + 1 n(n - 1) + 1 n(n−1)+1。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100;

int n, a[N + 10][N + 10];
int cnt[N * N + 10];

void work() {
    cin >> n;
    for (int i = 1; i <= n * n; ++i) cnt[i] = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            cin >> a[i][j]; ++cnt[a[i][j]];
        }
    }
    for (int i = 1; i <= n * n; ++i) {
        if (cnt[i] > n * (n - 1)) {
            cout << "NO" << '\n'; return;
        }
    }
    cout << "YES" << '\n';
}

int main() {
    int T; cin >> T;
    for (; T--;) work();
    return 0;
}

B - Cyclists

有 n n n 张牌排成队列,初始时目标牌在第 p p p 位。每回合可选择前 k k k 张牌中的任意一张,将其移出并重新放入队列末尾。每张牌有消耗 a i a_i ai,总消耗不得超过 m m m。求在满足总消耗限制的前提下,目标牌最多能被使用的次数。

数据范围: 1 ≤ t ≤ 5000 1 \le t \le 5000 1≤t≤5000, 1 ≤ k , p ≤ n ≤ 5000 1 \le k, p \le n \le 5000 1≤k,p≤n≤5000, 1 ≤ m ≤ 5000 1 \le m \le 5000 1≤m≤5000, 1 ≤ a i ≤ m 1 \le a_i \le m 1≤ai≤m,所有测试用例的 n n n 之和不超过 5000 5000 5000。

考虑贪心,若 win-condition 在前 k k k 张牌中直接取走,否则取走 a i a_i ai 最小的排。用个 vector 模拟即可做到 O ( n m ) O(n m) O(nm)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 5000;

int n, k, p, m;
int a[N + 10];
vector<pair<int, int> > q;

void work() {
    cin >> n >> k >> p >> m;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    q.clear();
    for (int i = 1; i <= n; ++i) q.push_back({a[i], (i == p ? 1 : 0)});
    int ans = 0;
    for (;;) {
        int flag = -1;
        for (int i = 0; i < k; ++i) {
            if (q[i].second) {
                flag = i; break;
            }
        }
        if (flag != -1) {
            if (m >= q[flag].first) {
                m -= q[flag].first, ++ans;
                pair<int, int> tmp = q[flag];
                q.erase(q.begin() + flag); q.push_back(tmp);
            }
            else break;
            continue;
        }
        int mi = 0x3f3f3f3f, pos = -1;
        for (int i = 0; i < k; ++i) {
            if (q[i].first < mi) mi = q[i].first, pos = i;
        }
        if (m >= mi) {
            m -= q[pos].first;
            pair<int, int> tmp = q[pos];
            q.erase(q.begin() + pos); q.push_back(tmp);
        }
        else break;
    }
    cout << ans << '\n';
}

int main() {
    int T; cin >> T;
    for (; T--;) work();
    return 0;
}

C - Stamina and Tasks

给定 n n n 个任务,每个任务 i i i 包含价值 c i c_i ci 和难度 p i p_i pi。初始体力 S = 1 S = 1 S=1。按顺序处理每个任务时:

  1. 放弃任务:体力 S S S 不变,获得 0 分。
  2. 完成任务:获得 S ⋅ c i S \cdot c_i S⋅ci 分,随后体力变为 S ⋅ ( 1 − p i 100 ) S \cdot (1 - \frac{p_i}{100}) S⋅(1−100pi)。

目标是合理选择任务以最大化获得的总分。

数据范围: 1 ≤ t ≤ 10 3 1 \le t \le 10^3 1≤t≤103, 1 ≤ n ≤ 10 5 1 \le n \le 10^5 1≤n≤105,所有测试点中 n n n 的总和不超过 10 5 10^5 105, 1 ≤ c i ≤ 100 1 \le c_i \le 100 1≤ci≤100, 0 ≤ p i ≤ 100 0 \le p_i \le 100 0≤pi≤100。

考虑 dp,设 f ( i ) f(i) f(i) 表示只考虑 i ∼ n i \sim n i∼n 的 tasks 时,答案的最大值。

不难得出状态转移方程: f ( i ) = max ⁡ { f ( i + 1 ) , s i + ( 1 − p i ) f ( i + 1 ) } f(i) = \max \{ f(i + 1), s_i + (1 - p_i) f(i + 1) \} f(i)=max{f(i+1),si+(1−pi)f(i+1)},时间复杂度 O ( n ) O(n) O(n)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100000;

int n;
double c[N + 10], p[N + 10], f[N + 10];

void work() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> c[i] >> p[i]; p[i] /= 100.0;
    }
    for (int i = 1; i <= n + 1; ++i) f[i] = 0.0;
    for (int i = n; i >= 1; --i) {
        f[i] = max(f[i + 1], c[i] + (1.0 - p[i]) * f[i + 1]);
    }
    cout << fixed << setprecision(10) << f[1] << '\n';
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T; cin >> T;
    for (; T--;) work();
    return 0;
}

D - Tree Orientation

给定一棵包含 n n n 个节点的无向树,对其每条边指定一个方向,使其成为有向图。现在已知该有向图中任意两点 u , v u, v u,v 之间的可达性矩阵(以 n n n 个长度为 n n n 的字符串给出)。请根据该可达性矩阵还原出原树的结构及边的方向。如果不存在合法的构造方案,输出 No;否则输出 Yes 并给出任意一种构造方案。

数据范围: 1 ≤ t ≤ 10 4 1 \le t \le 10^4 1≤t≤104, 2 ≤ n ≤ 8000 2 \le n \le 8000 2≤n≤8000,且所有测试用例中 n 2 n^2 n2 之和不超过 8000 2 8000^2 80002。

设 S u S_u Su 表示 u u u 能到达的点, E E E 为边集。

不难发现:若合法,则 S_u = u \\cup \\bigcup_{(u, v) \\in E} S_v ,且 ( u , v ) ∈ E (u, v) \in E (u,v)∈E 和 ( u , w ) ∈ E , w ≠ v (u, w) \in E, w \neq v (u,w)∈E,w=v 满足 S v ∩ S w = ∅ S_v \cap S_w = \emptyset Sv∩Sw=∅。

那么我们只需要先判掉环(这样子肯定是一个森林),然后判掉 u , v u, v u,v 满足 v ∈ S u v \in S_u v∈Su 且 ∣ S u ∣ ≤ ∣ S v ∣ |S_u| \leq |S_v| ∣Su∣≤∣Sv∣(根据上面的限制不难得出)。

然后按照上面的贪心找到所有的边,最后判断这个图是否联通即可,时间复杂度 O ( n 2 ) O(n^2) O(n2)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 8000;

int n;
bitset<N + 10> R[N + 10];
int cnt[N + 10], p[N + 10];
vector<pair<int, int> > ans;
vector<int> son[N + 10];
int fa[N + 10], sum;

int find(int i) {
    if (fa[i] == i) return i;
    return fa[i] = find(fa[i]);
}

bool cmp(int u, int v) {
    return cnt[u] > cnt[v];
}

void work() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        string s; cin >> s;
        R[i].reset(), cnt[i] = 0;
        for (int j = 1; j <= n; ++j) {
            if (s[j - 1] == '1') R[i][j] = 1, cnt[i]++;
        }
    }
    for (int i = 1; i <= n; ++i) {
        if (!R[i].test(i)) {
            cout << "No" << '\n'; return;
        }
        p[i] = i;
    }
    sort(p + 1, p + n + 1, cmp); ans.clear();
    for (int i = 1; i <= n; ++i) son[i].clear();
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (i != j && R[i][j] && cnt[j] >= cnt[i]) {
                cout << "No" << '\n'; return;
            }
        }
    }
    for (int i = 1; i <= n; ++i) {
        bitset<N + 10> todo = R[i]; todo.reset(i);
        for (int j = 1; j <= n; ++j) {
            int node = p[j];
            if (todo.test(node)) {
                ans.push_back({i, node});
                son[i].push_back(node);
                todo &= ~R[node];
                if (ans.size() >= n) {
                    cout << "No" << '\n'; return;
                }
            }
            if (todo.none()) break;
        }
        if (todo.any()) {
            cout << "No" << '\n'; return;
        }
    }
    if (ans.size() != n - 1) {
        cout << "No" << '\n'; return;
    }
    for (int i = 1; i <= n; ++i) fa[i] = i;
    sum = n;
    for (pair<int, int> e : ans) {
        int u = e.first, v = e.second;
        u = find(u), v = find(v);
        if (u != v) fa[u] = v, --sum;
    }
    if (sum != 1) {
        cout << "No" << '\n'; return;
    }
    for (int i = 1; i <= n; ++i) {
        long long c = 1;
        bitset<N + 10> tmp; tmp.set(i);
        for (int ch : son[i]) c += cnt[ch], tmp |= R[ch];
        if (c != (long long)cnt[i] || tmp != R[i]) {
            cout << "No" << '\n'; return;
        }
    }
    cout << "Yes" << '\n';
    for (pair<int, int> e : ans) {
        cout << e.first << ' ' << e.second << '\n';
    }
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T; cin >> T;
    for (; T--;) work();
    return 0;
}

E - Counting Cute Arrays

给定一个长度为 n n n 的正整数数组 A A A,定义数组 f ( A ) f(A) f(A),其中 f ( A ) i f(A)_i f(A)i 为满足 j < i j < i j<i 且 A j < A i A_j < A_i Aj<Ai 的最大下标 j j j;若不存在,则 f ( A ) i = 0 f(A)_i = 0 f(A)i=0。称满足存在某个数组 A A A 使得 f ( A ) = P f(A) = P f(A)=P 的数组 P P P 为"可爱数组"。

现给出一个长度为 n n n 的数组 X X X,其中部分元素为 − 1 -1 −1,其余为 0 0 0 到 n n n 之间的整数。你需要将所有 − 1 -1 −1 替换为 0 0 0 到 n n n 之间的整数,使得得到的数组 X ′ X' X′ 是一个"可爱数组",求满足条件的替换方案数,结果对 998 , 244 , 353 998,244,353 998,244,353 取模。

数据范围: 1 ≤ t ≤ 10 3 1 \le t \le 10^3 1≤t≤103, 1 ≤ n ≤ 5000 1 \le n \le 5000 1≤n≤5000,所有测试用例中 n n n 之和不超过 5000 5000 5000, − 1 ≤ X i ≤ n -1 \le X_i \le n −1≤Xi≤n。

不难发现以下性质:

  • P i < i P_i < i Pi<i,且连边 P i → i P_i \to i Pi→i,这是一棵以 0 0 0 为根的树(或森林)。
  • ∀ j ≠ i \forall j \neq i ∀j=i, [ P i , i ] [P_i, i] [Pi,i] 和 [ P j , j ] [P_j, j] [Pj,j] 不相交

(反证法略:若相交,根据寻找"左侧第一个更小值"的单调栈性质会产生矛盾)。

所以这棵树的节点是严格按照先序遍历 1 , 2 , ... , n 1, 2, \dots, n 1,2,...,n 加入的。

由于儿子是按先序遍历 1 ∼ n 1 \sim n 1∼n 加入的,当我们加入节点 i i i 时,它只能挂在当前树的右主干(即从根一直往右子节点走的路径)上某个节点的右儿子处。

  • 如果 i i i 挂在右主干的末端,则右主干长度 + 1 +1 +1;
  • 如果 i i i 挂在偏上的祖先,原本在它位置下面的右主干节点就被封死(不能再加儿子了),右主干长度变短。

同时,利用"区间不相交"这一极强的性质,既然给定的强制边 ( X i , i ) (X_i, i) (Xi,i) 构成的区间 [ X i , i ] [X_i, i] [Xi,i] 绝对不能交叉,这就意味着一个闭合区间内部的连边方案,与区间外部是完全独立的!

因此,我们可以先将所有 X i ≠ − 1 X_i \neq -1 Xi=−1 视为区间 [ X i , i ] [X_i, i] [Xi,i]。利用单调栈扫描一遍,若发现区间交叉则直接输出 0 0 0;若合法,这些区间会形成一棵包含关系的树。

然后由于每个区间都是独立的,我们可以 dp。

设 f ( i , j ) f(i, j) f(i,j) 表示考虑到该序列的第 i i i 个节点,当前右主干上可用节点数为 j j j 的方案数。

  • 若 X i = − 1 X_i = -1 Xi=−1:它可以挂在主干上的第 k k k 个节点( k ≥ j − 1 k \ge j - 1 k≥j−1),挂完后主干长度变为 j j j,即 f ( i , j ) = ∑ k ≥ j − 1 f ( i − 1 , k ) f(i, j) = \sum_{k \geq j - 1} f(i - 1, k) f(i,j)=∑k≥j−1f(i−1,k)。用后缀和优化即可做到 O ( 1 ) O(1) O(1) 转移。

  • 若 X i ≠ − 1 X_i \neq -1 Xi=−1:由于它必须挂在子区间的左端点(即当前主干的最末端),它不能弹出主干上的任何节点,只能强制在顶端新增一层,即 f ( i , j ) = f ( i − 1 , j − 1 ) f(i, j) = f(i - 1, j - 1) f(i,j)=f(i−1,j−1)。

最终整个数组的答案就是所有独立序列方案数的乘积,时间复杂度 O ( n 2 ) O(n^2) O(n2)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 5000;
const long long mod = 998244353LL;

struct node {
    int l, r, id;
    bool operator < (node other) const {
        if (l != other.l) return l < other.l;
        return r > other.r;
    }
};

int n, x[N + 10], fa[N + 10];
long long dp[2][N + 10];
vector<node> interval, st;
vector<int> son[N + 10];

long long solve(vector<int> seq) {
    if (seq.empty()) return 1LL;
    int sz = seq.size();
    for (int i = 0; i <= sz + 1; ++i) dp[0][i] = dp[1][i] = 0LL;
    dp[1][1] = 1LL;
    for (int i = 0; i < sz; ++i) {
        int cur = i & 1, lst = (i & 1) ^ 1;
        for (int j = 0; j <= sz + 1; ++j) dp[cur][j] = 0LL;
        if (!seq[i]) {
            long long sum = 0LL;
            for (int j = sz + 1; j >= 2; --j) {
                sum = (sum + dp[lst][j - 1]) % mod; dp[cur][j] = sum;
            }
        }
        else {
            for (int j = 2; j <= sz + 1; ++j) dp[cur][j] = dp[lst][j - 1];
        }
    }
    long long res = 0LL;
    for (int i = 0; i <= sz + 1; ++i) res = (res + dp[(sz - 1) & 1][i]) % mod;
    return res;
}

void work() {
    cin >> n;
    for (int i = 1; i <= n + 1; ++i) fa[i] = -1, son[i].clear();
    for (int i = 1; i <= n; ++i) cin >> x[i];
    for (int i = 1; i <= n; ++i) {
        if (x[i] >= i) {
            cout << 0 << '\n'; return;
        }
    }
    interval.clear(); st.clear();
    interval.push_back((node){0, n + 1, n + 1});
    for (int i = 1; i <= n; ++i) {
        if (x[i] != -1) interval.push_back((node){x[i], i, i});
    }
    sort(interval.begin(), interval.end());
    for (int i = 0; i < (int)interval.size(); ++i) {
        node cur = interval[i];
        while (!st.empty()) {
            if (st.back().r <= cur.l) st.pop_back();
            else break;
        }
        if (!st.empty()) {
            if (cur.r > st.back().r) {
                cout << 0 << '\n'; return;
            }
            fa[cur.id] = st.back().id;
        }
        st.push_back(cur);
    }
    for (int i = 1; i <= n; ++i) {
        if (x[i] != -1 && fa[i] != -1) son[fa[i]].push_back(i);
    }
    long long ans = 1LL;
    for (int i = 1; i <= n + 1; ++i) {
        if (i <= n && x[i] == -1) continue;
        sort(son[i].begin(), son[i].end());
        vector<int> seq; int cur = (i == n + 1 ? 0 : x[i]) + 1;
        for (int j = 0; j < (int)son[i].size(); ++j) {
            int u = son[i][j];
            while (cur <= x[u]) seq.push_back(0), ++cur;
            seq.push_back(1), cur = u + 1;
        }
        while (cur < i) seq.push_back(0), ++cur;
        ans = ans * solve(seq) % mod;
    }
    cout << ans << '\n';
}

int main() {
    int T; cin >> T;
    for (; T--;) work();
    return 0;
}
相关推荐
Trouvaille ~1 小时前
【贪心算法】专题(五):逆向思维与区间重叠的极致拉扯
c++·算法·leetcode·青少年编程·面试·贪心算法·蓝桥杯
Flying pigs~~2 小时前
深度学习之人工神经网络总结
人工智能·深度学习·算法·ann·人工神经网络
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践3 kicad routing tools 从PCB文件获取了哪些信息
算法·agent·pcb·eda·routing
2401_898075122 小时前
代码生成器优化策略
开发语言·c++·算法
郝学胜-神的一滴2 小时前
人工智能发展漫谈:从专家系统到AIGC,再探深度学习核心与Pytorch入门
人工智能·pytorch·python·深度学习·算法·cnn·aigc
nananaij2 小时前
【LeetCode-03 判断根结点是否等于子结点之和 python解法】
python·算法·leetcode
超级大只老咪2 小时前
差分算法(java)
算法
逆境不可逃2 小时前
【从零入门23种设计模式21】行为型之空对象模式
java·开发语言·数据库·算法·设计模式·职场和发展
超级大只老咪2 小时前
输入(java)
算法