Codeforces Round 1086 (Div. 2) 题解
Codeforces Round 1086 (Div. 2) 题解
我打赢复活赛了!
Problem A. Bingo Candies
给你一个 \(n\times n\) 的数字矩阵。求将数字重排后,是否存在一种重排方案使得任意一行或者一列的数字种类大于 \(2\)
猜结论的题。只要任何一种数字数量都不超过 \(n(n-1)\) 就够了。
int n;
int a[200][200];
void solve() {
int n;
cin >> n;
std::vector<int> cnt(n *n + 10);
int mx = 0;
for (int i = 1, x; i <= n * n; i++) {
cin >> x, cnt[x]++;
upmax(mx, cnt[x]);
}
cout << (mx <= (n * (n - 1)) ? "Yes" : "No") << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem B. Cyclists
初始有 \(n\) 张牌,第 \(i\) 张牌有一个打出代价 \(a_i\)。每次可以任意打出前 \(k\) 张牌中的任意一张,打出后将牌放到队尾。求总代价不超过 \(m\) 的情况下,第 \(p\) 张牌最多能打出多少次。
比较考验眼力劲儿。
我一开始读错题了,以为牌在哪个位置就需要花费 a[i] 的代价……
如果 win-condition card(胜利条件卡)本来就在前 k 位,那么很显然直接打出即可。
如果不在,那么贪心地想,你需要打出 p-k 张其他的牌来将胜利条件卡挤到最前面。
通过手玩会发现,无论牌的顺序如何,最优的情况一定是打出最小的 p-k 张牌,并且一定可以打出这 p-k 张最小牌。
同理可以得到打出牌之后的情况,情况同上这里不再赘述。
int n, k, p, m;
int a[kMaxN];
int b[kMaxN];
int pre[kMaxN];
void solve() {
cin >> n >> k >> p >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
int cst = a[p];
if (p > k) {
std::sort(a + 1, a + p);
int need = p - k;
for (int i = 1; i <= need; i++) cst += a[i];
}
if (cst > m) return cout << 0 << endl, void();
m -= cst, cst = a[p];
if (n > k) {
std::swap(a[p], a[n]);
std::sort(a + 1, a + n);
int need = n - k;
for (int i = 1; i <= need; i++) cst += a[i];
}
cout << 1 + m / cst << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem C. Stamina and Tasks
初始体力 \(S=1\)。有 \(n\) 个任务,不可以调换任务顺序。
你可以选择执行任务,这样获得 \(S \times c_i\) 的收益并且体力变为 \(S \times (1-\frac{p_i}{100})\)。
如果不执行任务,那么什么都不会发生,并走到下一个任务。
正着做很难写,状态涉及到收益和体力。
但是倒着做就很显然了,对于当前的总收益 \(t\),如果你选择执行第 \(i\) 个任务,那么收益会变成 \(t \times (1-\frac{p_i}{100}) + c_i\)。因为体力减少是乘累计的,这样就可以打包计算所有后续收益。
int n, S;
int p[kMaxN], c[kMaxN];
void solve() {
S = 1;
cin >> n;
for (int i = 1; i <= n; i++) cin >> c[i] >> p[i];
double ans = 0;
for (int i = n; i >= 1; i--) {
double c = ::c[i];
double p = 1.0 - (::p[i] / 100.0);
upmax(ans, c + p * ans);
}
printf("%.10lf\n", ans);
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem D. Tree Orientation
给你一个矩阵 \(s\),其中 \(s_{i, j}\) 表示 \(i\) 是否可以到达 \(j\)。要求构造出一个有向树,使得其满足这种到达关系,或者判定不存在。
两个难度仅仅是 \(n\) 范围内不同,一个 \(500\), 一个 \(8000\)。
为了方便起见,点 \(u\) 可到达的点集合记作 \(S_u\),可以到达 \(u\) 的点集合记作 \(T\)。
考虑树边 \((u, v)\) 是否存在,有两种判定方式:
-
不存在一个点 \(k\),使得 \(u\) 可以到达 \(k\) 并且 \(k\) 可以到达 \(v\)。这样 \(u\) 和 \(v\) 就是直接相连。
-
树边存在的一个必要条件就是 \(v\) 的可到达集合一定是 \(u\) 可到达集合的子集。同时不存在另外一个点 \(k\),使得 \(S_v \subset S_k \subset S_u\),此时 \((u, v)\) 树边一定存在。
第一种直接写可以写出 \(n^3\) 的判定,看上去可能会 TLE。(嗯没错我一开始就 TLE 了)所以可以使用 bitset 优化一下,看 \(S_u\) 和 \(T_v\) 的交集是否为 \(2\)。
第二种判定方法看上去和第一种没有本质区别,但是它可以引出一个贪心:
将 \(S_u\) 中的所有点 \(p\) 按照 \(|S_p|\) 的大小从大往下排。这样当你遍历时,原先遍历的点越有可能是 \(u\) 的儿子。一旦你确定 \(p\) 是 \(u\) 的儿子,那么将 \(S_p\) 中所有点打上标记记作已经是子树节点,这样后续遍历的时候直接跳过即可。
容易知道 \(u\) 所有儿子的 \(S_i\) 一定是互不相交的。
这里可能会引出一个问题:如果某个 \(p \in S_u\),但是 \(S_p \not\subset S_u\) 呢?说明原树不存在,交给后续判定。
确定的树边一定是 \(n-1\) 并且在无向图下所有点联通,否则原树不存在。
然后 \(O(n^2)\) 基于选定树边构造 \(S^\prime\) 就可以了。
不过这里有个关于异或哈希的小巧思:为每一个点 \(p\) 赋予一个随机权值 \(val_p\)。如果要判定 \(S_i\) 是否等于 \(S^\prime_i\),直接将集合内所有点的值异或起来然后判断是否相等就可以了。由于只转移异或值,会相当好转移。这样能将判定的复杂度降到 \(O(n)\)。(当然没什么卵用就是了)
(我说是一开始算错了复杂度以为链会退化到 \(O(n^3)\) 你信吗。)
#include <algorithm>
#include <iostream>
#include <random>
#include <string>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 8000 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
// 如果 u 可以直接到达 v,那么 v 的到达集合一定是 u 的子集
// 所以贪心就可以得到直接出边
int n;
std::string a[kMaxN];
int sum[kMaxN];
int val[kMaxN];
std::vector<int> go[kMaxN];
bool used[kMaxN];
int fa[kMaxN];
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int mem[kMaxN];
int qry(int u) {
if (mem[u]) return mem[u];
mem[u] = val[u];
for (auto v : go[u]) {
mem[u] ^= qry(v);
}
return mem[u];
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) go[i].clear();
for (int i = 1; i <= n; i++) used[i] = false;
for (int i = 1; i <= n; i++) mem[i] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i], a[i] = '#' + a[i];
sum[i] = 0;
for (int j = 1; j <= n; j++) {
if (a[i][j] - '0') sum[i] ^= val[j], go[i].push_back(j);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j && a[i][j] == '0') return cout << "No" << endl, void();
if (i != j && a[i][j] == '1' && a[j][i] == '1') return cout << "No" << endl, void();
}
}
std::vector<std::pair<int, int>> edge;
for (int i = 1; i <= n; i++) fa[i] = i;
for (int u = 1; u <= n; u++) {
std::vector<int> id;
for (auto v : go[u]) {
if (v == u) continue;
id.push_back(v);
}
std::sort(id.begin(), id.end(), [](int i, int j) {
return go[i].size() == go[j].size() ? i < j : go[i].size() > go[j].size();
});
for (auto v : id) {
if (used[v]) continue;
used[v] = true;
edge.push_back({u, v});
fa[find(u)] = find(v);
for (auto j : go[v]) used[j] = true;
}
for (auto v : id) used[v] = false;
}
if (edge.size() != n - 1) return cout << "No" << endl, void();
int ccnt = 0;
for (int i = 1; i <= n; i++) ccnt += fa[i] == i, go[i].clear();
if (ccnt > 1) return cout << "No" << endl, void();
for (auto& [u, v] : edge) go[u].push_back(v);
for (int i = 1; i <= n; i++) {
// cout << qry(i) << ' ' << sum[i] << endl;
if (qry(i) != sum[i]) return cout << "No" << endl, void();
}
cout << "Yes" << endl;
for (auto& [u, v] : edge) cout << u << ' ' << v << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
for (int i = 0; i < kMaxN; i++) val[i] = rand(1, 1e9);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem E. Counting Cute Arrays
对于一个序列 \(X\),定义它为可爱的序列,当且仅当存在一个序列 \(A\),可以通过如下步骤定义一个函数 \(f(A)\) 生成 \(f(A)=X\):
- 对于任意 \(i\),定义 \(X_i=j\) 时要求 \(j < i\) 并且 \(A_j < A_i\)。如果有多个满足条件的 \(j\),则选取最靠右的 \(j\)。
给定一个 \(X\),如果 \(X_i=-1\) 则表示这个位置待定。问有多少种 \(X\) 是可爱的。
\(n\leq 5000\)
\(X\) 是可爱的序列的充要条件是:
- \(\forall i, X_i < i\)。
- 不存在 \(j < i\),使得 \(X_j < X_i < j < i\)。
第一个比较显然,对于第二个条件的必要性也是很显然的:
- 如果存在这样的 \(j < i\),那么 \(A_{X_j} < A_j, A_{X_i} > A_j\),但是又 \(A_{X_i} < A_i\),所以 \(X_i\) 应该为 \(j\),矛盾。所以不存在。
至于第二个条件的充分性,可以手玩发现对于一个给定的 \(X\),在满足第二个条件的情况下一定可以构造出一个满足条件的 \(A\) 使得 \(f(A)=X\)。具体流程不赘述。
这个充要命题的抽象是必要的。可以将 \((X_i, i)\) 视作一个区间,容易发现这个条件就是要求任意区间只能是包含、相离的关系。当然,端点相交是可以的。
对于一个确定的 \((X_i, i)\),可以知道未赋值的 \(X_j, j > i\) 是无论如何都无法将 \(X_j\) 复制到 \((x_i, i)\) 里面的。所以对于一个边界确定的子区间,它的计数是独立且不受外界影响的。
于是我们考虑去 \(dp\) 一个边界确定的 \((X_i, i)\) 来计数符合要求的条件数。容易知道只有 \(j \in [X_i + 1, i - 1]\) 的 \(X_j\) 是可以被修改的,且左端点不能超过 \(X_i\)。
当我们 \(dp\) 处理 \((X_i, i)\) 时,会有若干被完全包裹的边界确定的子区间,这些区间应当当作子问题处理后再转移。比较值得一提的是,这些完全包裹的关系可以构成一颗树。
- 这个包裹关系需要处理好,我们举一个例子,当你手上有三个区间:\(0:(1, 10), 1:(1, 5), 2:(1, 3)\) 时,显然构成的关系是 \(0->1->2\),在处理 \(0\) 的 \(dp\) 转移时,你的左端点可以选取到 \(5\) 但是绝对不能选取到 \(4\)。
除去子区间以外剩余需要被重新赋值的 \(X_p\),由于我们需要知道有哪些左端点可以被选取,而可以被选定的左端点数量比较适合作为状态进行转移,于是可以记状态 \(dp[p][c]\) 为 \(p\) 赋值完后、剩余 \(c\) 个端点可以被当作左端点进行选取。
-
如果 \(p\) 是某个子区间内的点,显然不应该由当前问题去考虑转移,而是交给独立子问题去处理。
-
当 \(p\) 作为某个直接子区间的右端点时,它显然无法被赋值,但是依旧可以担任后续赋值的左端点,此时状态转移:
- 剩余一般情况,对于 \(dp[p - 1][c]\),它可以转移到 \(\forall c^\prime < c, dp[p][c^\prime + 1]\) 中。因为你的赋值使得中间可选左端点变得不再可选,所以能够如此转移。比如 \(c=4\) 时,有四个左端点 \(T_1, T_2, T_3, T_4\),如果你选择 \(T_1\) 作为左端点,那么其余三个在后续转移中将不再可用。同时 \(p\) 本身也会在后续中担任左端点作用,所以要 \(+1\)。
于是这个题就写完了。
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 5e3 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
const int MOD = 998244353;
struct modint {
int val;
modint(long long v = 0) {
if (v < 0) v = v % MOD + MOD;
if (v >= MOD) v %= MOD;
val = (int)v;
}
explicit operator int() const { return val; }
friend modint qpow(modint a, long long p) {
modint ans(1);
while (p > 0) {
if (p & 1) ans *= a;
a *= a, p >>= 1;
}
return ans;
}
modint inv() const { return qpow(*this, MOD - 2); }
modint& operator+=(modint b) {
val += b.val;
if (val >= MOD) val -= MOD;
return *this;
}
modint& operator-=(modint b) {
val -= b.val;
if (val < 0) val += MOD;
return *this;
}
modint& operator*=(modint b) {
val = (int)(1LL * val * b.val % MOD);
return *this;
}
modint& operator/=(modint b) { return *this *= b.inv(); }
friend modint operator+(modint a, modint b) { return a += b; }
friend modint operator-(modint a, modint b) { return a -= b; }
friend modint operator*(modint a, modint b) { return a *= b; }
friend modint operator/(modint a, modint b) { return a /= b; }
};
int n;
int dp[kMaxN][kMaxN];
int a[kMaxN];
struct line {
int l, r;
bool operator<(const line& b) const {
if (l != b.l) return l < b.l;
return r > b.r;
}
};
void solve() {
cin >> n;
bool flag = true;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] >= i) flag = false;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (a[i] == -1 || a[j] == -1) continue;
// 判定是否出现交叉情况
if (a[j] < a[i] && a[i] < j) flag = false;
}
}
if (flag == false) return cout << 0 << endl, void();
// 左端点可能有很多个,但是右端点是一定的
std::vector<line> s;
std::vector<int> stk;
s.push_back({0, n + 1});
for (int i = 1; i <= n; i++) {
if (~a[i]) s.push_back({a[i], i});
}
std::sort(s.begin(), s.end());
std::vector<std::vector<int>> go(s.size() + 1);
for (int i = 0; i < s.size(); i++) {
while (!stk.empty() && s[stk.back()].r < s[i].r) stk.pop_back();
if (!stk.empty()) go[stk.back()].push_back(i);
stk.push_back(i);
}
auto dfs = [&](auto& self, int u) -> modint {
// cerr << u << endl;
modint ans1 = 1;
for (auto v : go[u]) ans1 *= self(self, v);
int L = s[u].l, R = s[u].r, len = R - L + 1;
std::vector<modint> dp(len + 2, 0);
dp[1] = 1;
std::vector<bool> inChild(R + 2, false), Right(R + 2, false);
for (auto v : go[u]) {
Right[s[v].r] = true;
for (int p = s[v].l + 1; p <= s[v].r - 1; p++) inChild[p] = true;
}
for (int p = L + 1; p <= R - 1; p++) {
if (inChild[p]) continue;
std::vector<modint> nxt(len + 2, 0);
if (Right[p]) {
for (int c = 1; c <= len; c++) nxt[c + 1] = dp[c];
} else {
modint sum = 0;
for (int c = len; c >= 1; c--) {
sum += dp[c];
nxt[c + 1] = sum;
}
}
dp = std::move(nxt);
}
modint ans = 0;
for (int c = 1; c <= len; c++) ans += dp[c];
return ans1 * ans;
};
cout << (int)dfs(dfs, 0) << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}

浙公网安备 33010602011771号