笑点解析:打到一半睡着了(还好是 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。按顺序处理每个任务时:
- 放弃任务:体力 S S S 不变,获得 0 分。
- 完成任务:获得 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;
}