Educational Codeforces Round 188 (Rated for Div. 2) 题解
Educational Codeforces Round 188 (Rated for Div. 2) 题解
edu 场难度感觉,平均上低于 div. 2
Problem A. Passing the Ball
有
n个人传球。每个人拿到球以后会朝右或者左传。保证不超过边界的情况下,球最多可能经过几个人。
对着字符串模拟就可以了。
(我这个笨蛋一开始甚至读错题了)
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
int t;
bool vis[kMaxN];
void solve() {
int n;
std::string str;
cin >> n >> str;
std::vector<bool> vis(n + 1, false);
int p = 1;
vis[p] = true;
for (int i = 1; i <= n; i++) {
if (str[p - 1] == 'R') p++;
else p--;
vis[p] = true;
}
int ans = 0;
for (auto i :vis) ans += i;
cout << ans << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem B. Right Maximum
每次挑选最靠右的最大值,然后删除最大值右边的所有元素。问一共要删除几次。
斯大林排序?
维护一个前缀 max,容易知道如果 \(a_i = max_i\),那么这个数将会被选取并被删除。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
int n;
int a[kMaxN];
int max[kMaxN];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], max[i] = a[i];
for (int i = 1; i <= n; i++) upmax(max[i], max[i - 1]);
int ans = 0;
for (int i = n; i >= 1; i--) {
if (a[i] == max[i]) ans++;
}
cout << ans << 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. Spring
每个人分别会间隔 \(a, b, c\) 天来喝水。每天一共 \(6\) 升水并且均分给所有来喝水的人。问每一个人喝多少水。
容斥原理。可以计算出每组人要隔多少天来喝水,然后容斥就可以了。
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
void solve() {
int a, b, c;
int m;
cin >> a >> b >> c >> m;
int A = m / a;
int B = m / b;
int C = m / c;
int AB = m / std::lcm(a, b);
int BC = m / std::lcm(c, b);
int AC = m / std::lcm(a, c);
int ABC = m / std::lcm(std::lcm(a, b), c);
int ansa = (__int128)A * 6 - 3 * AB - 3 * AC + 2 * ABC;
int ansb = (__int128)B * 6 - 3 * AB - 3 * BC + 2 * ABC;
int ansc = (__int128)C * 6 - 3 * AC - 3 * BC + 2 * ABC;
cout << ansa << ' ' << ansb << ' ' << ansc << 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 D. Alternating Path
题目大意
给定一个 \(n\) 个节点、\(m\) 条边的无向简单图(无自环和重边)。
你需要给每条边确定一个方向,将其变为有向图。定义交替路径为顶点序列 \(v_1, v_2, \dots, v_k\)(长度任意,可包含重复节点),满足相邻边的方向交替变化,即:
- \(v_1 \to v_2\)
- \(v_3 \to v_2\)
- \(v_3 \to v_4\)
- \(v_5 \to v_4\)
- ……依此类推(出边、入边、出边、入边交替出现)。
定义美丽点:如果从某个顶点 \(v\) 出发的所有可能路径(在原图中的任何路径)在定向后都满足“交替路径”的条件,则称点 \(v\) 为美丽点。
任务
求通过最优的边定向方案,最多能得到多少个美丽点?
手玩一下会发现如果一个图内,某个点无法成为美丽点,那么整个图都无法找到任何一个美丽点。
同时会发现有美丽点当且仅当图内不存在奇环。
于是 0/1 染色判定就可以了。
如果手搓一个树就会发现两种颜色的分布并不是相等的甚至不是除以 \(2\) 关系。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
int n, m;
int col[kMaxN];
std::vector<int> go[kMaxN];
int cnt[3];
bool flag = true;
void dfs(int u, int c) {
if (~col[u]) return;
col[u] = c;
cnt[c]++;
for (auto v : go[u]) {
if (~col[v]) {
if (col[v] == c) flag = false;
} else {
dfs(v, c ^ 1);
}
}
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; i++) col[i] = -1, go[i].clear();
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
go[u].push_back(v), go[v].push_back(u);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (col[i] == -1) {
flag = true, cnt[0] = cnt[1] = 0;
dfs(i, 1);
if (flag) ans += std::max(cnt[0], cnt[1]);
}
}
cout << ans << 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 E. Sum of Digits (and Again)
初始时有一个数 \(x\)。定义 \(S(x)\):不断将 \(x\) 拼接到字符串 \(S(x)\) 之后,并且将 \(x\) 重新赋值为各数码之和。如果某次拼接之前 \(x \leq 9\),那么停止这个操作。
给定各数位打乱的 \(s\),请将 \(s\) 重排之后使其成为一个 \(S(x)\),即可以由某个 \(x\) 生成。
方便讨论记 \(f(x)\) 为 \(x\) 各数码之和。
手推一下,会知道 \(f(s) = f(S(x))=v_1 + v_2 + \cdots\),其中 \(v_1, v_2 \cdots\) 是生成过程中的数码和。容易知道 \(v_1, v_2, \cdots\) 一定是 \(S(x)\) 的子串。
事实上在 \(x\) 全为 \(9\) 的情况下,\(v_1\) 的上限也就是只是 \(9\times 10^5\),那么 \(v_2\) 就更小了,甚至不会超过 \(100\)。以此类推容易发现 \(v_1\) 才是 \(f(S(x))\) 的大头,而且不会比上限低多少。
所以直接枚举 \(v_1\) 就可以了。容易知道 \(f(v_1) = v_2, f(v_2) = v_3 \cdots\),于是可以通过这种方式生成字符串的尾部并且判定合法性。
值得注意的是实际上应当类似 \(f(S(x)) = v_1 + v_2 + v_3 + v_3\),并且个位数情况需要特判。
#include <iostream>
#include <random>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
void solve() {
std::string str;
cin >> str;
if (str.size() == 1) return cout << str << endl, void();
std::vector<int> cnt(10, 0);
int sum = 0;
for (auto& i : str) cnt[i - '0']++, sum += i - '0';
for (int v1 = std::max(1, sum - 500); v1 <= sum; v1++) {
std::vector<int> v(1, v1);
int now = v1, s = v1;
while (now > 9) {
int p = 0;
while (now) p += now % 10, now /= 10;
s += now = p;
v.push_back(now);
}
s += v.back();
if (s != sum) continue;
std::vector<int> cst(10, 0);
for (auto i : v) {
while (i) cst[i % 10]++, i /= 10;
}
bool flag = true;;
for (int i = 0; i <= 9; i++) {
if (cst[i] > cnt[i]) flag = false;
}
if (!flag) continue;
// 或许写到这里会有人感到疑惑:
// 你没有判定 v1 是否可以被剩下的数字加出来?
// 实际上这是一定的。
// 不妨将原字符串划分为:S(x)=s1s2s3
// 其中 f(s1) = s2 = v1, f(s2) = s3 = v2, f(s3) = s3 = v2
// 当我满足f(S(x)) = v1 + v2 + v2 时,f(s1) = f(S(x)) - v1 - v2 是必然成立的。
for (int i = 9; i >= 0; i--) {
cout << std::string(cnt[i] - cst[i], i + '0');
}
for (auto i : v) cout << i;
cout << endl;
return;
}
}
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 F. Sum of Fractions
操作定义:
对于一个分数 \(\frac{x}{y}\),你可以执行以下两种操作之一来增大它的值:
- 将分子 \(x\) 加 \(1\)(变为 \(\frac{x+1}{y}\))。
- 若分母 \(y > 1\),将分母 \(y\) 减 \(1\)(变为 \(\frac{x}{y-1}\))。
(注意:操作过程中分数不进行约分。例如 \(\frac{3}{7}\) 分母减 \(1\) 变为 \(\frac{3}{6}\),而不是 \(\frac{1}{2}\)。)函数 \(\text{MSF}(b, k)\) 定义:
给定一个整数数组 \(b\) 和操作次数 \(k\)。
- 构造一个分数数组:\(\frac{1}{b_1}, \frac{1}{b_2}, \dots, \frac{1}{b_{|b|}}\)。
- 对这些分数总共执行恰好 \(k\) 次上述增大操作(次数可任意分配给各个分数)。
- \(\text{MSF}(b, k)\) 表示操作完成后,所有分数之和能达到的最大值。
所求目标:
给定一个长度为 \(n\) 的数组 \(a\) 以及一个长度为 \(m\) 的操作次数数组 \(k\)(保证 \(k\) 非降序排列)。
对于每个 \(k_i\),计算数组 \(a\) 的所有连续子数组的 \(\text{MSF}\) 之和,并对 \(998244353\) 取模。即计算:\[\sum_{l=1}^{n} \sum_{r=l}^{n} \text{MSF}(a[l \dots r], k_i) \pmod{998244353} \]
我的写法和官方题解不太一样,但是思路应该没有什么本质区别
首先考虑 \(MSF\) 要怎么计算。
一个很显然的事情就是对着 \(b_i\) 最小的那个猛猛攻击。如果 \(k\) 足够大,将分母减到 \(1\) 后在加分子。否则简单计算一下会知道全加分子更优。
于是可以自然考虑哪些 \(a_i\) 会在哪些区间里担任最小值。这是个很经典的问题,可以用单调栈或者笛卡尔树处理。
事实上不论 \(k\) 等于多少,不会成为最小值的 \(a_i\) 的贡献是一定的。所以可以提前处理出来。即考虑当 \(a_i\) 作为最小值的时候、统计区间内其他的 \(1/a_p\) 之和。
手玩容易发现区间被 \(a_i\) 分成左右两边,单边贡献次数是梯状乘以另外一边宽度。
接下来考虑作为最小值的时候 \(a_i\) 的贡献怎么算:
- 不妨现将 \(a_i\) 排序,记 \(i\) 为 \(a_i\) 作为最小值的区间个数。然后考虑每一个 \(k\) 的答案。
- 可以知道,存在一个分界点 \(p\),使得 \(\forall i \leq p, a_i \leq k\)。
- 此时答案为:\(\displaystyle\sum_{i=1}^{p}{(k+2 - a_i) \times cnt_i} + \displaystyle\sum_{i = p + 1}^{n}\frac{k + 1}{a_i}cnt_i\)
此时经过简单的变形就可以得知,维护三个前缀和:\(a_i\times cnt_i, cnt_i, \frac{cnt_i}{a_i}\) 即可。
代码:
#include <iostream>
#include <algorithm>
#include <stack>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
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, m;
modint a[kMaxN];
modint k[kMaxN];
int L[kMaxN], R[kMaxN], id[kMaxN];
modint s[kMaxN], s2[kMaxN];
modint cnt[kMaxN], scnt[kMaxN];
modint v1[kMaxN], v2[kMaxN];
modint qry(int l, int r) {
return s2[r] - s2[l - 1] - (l - 1) * (s[r] - s[l - 1]);
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m;
for (int i = 1, x; i <= n; i++) {
cin >> x, a[i] = x, id[i] = i;
}
std::sort(id + 1, id + 1 + n, [](int i, int j) {
return a[i].val < a[j].val;
});
for (int i = 1, x; i <= m; i++) {
cin >> x, k[i] = x;
}
std::stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()].val >= a[i].val) {
R[st.top()] = i - 1, st.pop();
}
if (st.empty())
L[i] = 1;
else
L[i] = st.top() + 1;
st.push(i);
}
while (!st.empty()) R[st.top()] = n, st.pop();
modint all = 0;
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + 1 / a[i], s2[i] = s2[i - 1] + i / a[i];
cnt[i] = 1ll * (R[i] - i + 1) * (i - L[i] + 1);
}
for (int i = 1; i <= n; i++) {
modint lv = qry(L[i], i - 1), rv = (R[i] - i + 1) * (s[R[i]] - s[i]) - qry(i + 1, R[i]);
all += lv * (R[i] - i + 1) + rv * (i - L[i] + 1);
int p = id[i];
scnt[i] = scnt[i - 1] + cnt[p];
v1[i] = v1[i - 1] + a[p] * cnt[p];
v2[i] = v2[i - 1] + cnt[p] / a[p];
}
cerr << (int)all << endl;
for (int i = 1, j = 0; i <= m; i++) {
while (j <= n && a[id[j]].val - 1 <= k[i].val) j++;
int p = j - 1;
modint ans = all;
ans += (k[i] + 2) * scnt[p] - v1[p] + (k[i] + 1) * (v2[n] - v2[p]);
cout << (int)ans << endl;
}
return 0;
}
Problem G. Grid Path
在一个 \(n \times m\) 的网格中,有一枚棋子初始位于 \((1, 1)\)。
棋子每次可以向左、向右或向下移动一格,但不能越出边界。路径定义:棋子在移动过程中访问过的所有单元格构成的集合。
(注意:集合只记录哪些格子被走过,与访问顺序和次数无关)。目标:计算从 \((1, 1)\) 出发,棋子可以形成的路径集合总数。结果对 \(mod\) 取模。
这个题 AI 帮我写了一半
考虑暴力,很容易设计 \(dp\) 状态 \(dp[k][l][r]\),表示第 \(k\) 行经过了 \((l, r)\) 这个区间的路径数量。同时状态 \(dp[k - 1][l^\prime][r^\prime]\) 可以转移到 \(dp[k][l][r]\) 当且仅当 \((l, r)\) 与 \((l^\prime, r^\prime)\) 有交集。
由于直接考虑转移非常麻烦,枚举的复杂度级别是 \(O(m^4)\) 的,你连一轮转移都做不完就似的不知道哪里去了。
考虑正难则反,只需要将所有状态减去一定不成立的状态即可。
不妨设计两个前后缀和数组 \(L[i], R[i]\),定义为 \(L[i] = \displaystyle\sum_{r = 1}^{i}\sum_{l=1}^{r} dp[k-1][l][r], R[i] = \displaystyle\sum_{r=i}^{m}\sum_{l=i}^{r}dp[k - 1][l][r]\),定义所有方案和为 \(S\)。容易知道 \(S=L[m]=R[1]\)。
此时转移会非常显然:\(dp[k][l][r] = S - L[i - 1] - R[i + 1]\)
你会发现 \(L[i], R[i]\) 在每一个阶段之间的转移此时也确定了,并且转移一定可以被上一轮的 \(L, R\) 线性标出。
我们不妨将 \(L, R\) 直接压缩到一个向量中,这个向量维度大概是 \(O(2m)\) 级别。根据上面那坨式子,这样我们就可以上矩阵乘法了。
这个式子其实可以化简,这样就可以直接算出贡献的比例系数。
当然我比较懒,就直接跑 \(O(m^3)\) 的计算来累计贡献了。
当然还有一些代码细节,我就不太想讲了,像是要额外再给 \(ans\) 也就是答案之类的开一维。到这里应该比较显然了。
不过这个题需要卡常:要上矩阵转置并且利用一个技巧来减少取模次数。具体技巧可以直接看代码。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 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);
}
int n, m, MOD;
int inc(int x, int y) {
return x + y >= MOD ? x + y - MOD : x + y;
}
int dec(int x, int y) {
return x - y < 0 ? x - y + MOD : x - y;
}
int mul(int x, int y) {
return 1ll * x * y % MOD;
}
int S;
struct matrix {
int a[305][305];
matrix() {
for (int i = 0; i < 305; i++)
for (int j = 0; j < 305; j++) a[i][j] = 0;
}
int* operator[](int k) { return a[k]; }
const int* operator[](int k) const { return a[k]; }
matrix operator*(const matrix& b) const {
matrix tmp;
static int tr[305][305];
for (int i = 0; i <= S; ++i) {
for (int j = 0; j <= S; ++j) {
tr[j][i] = b.a[i][j];
}
}
for (int i = 0; i <= S; i++) {
for (int j = 0; j <= S; j++) {
unsigned long long sum = 0;
for (int k = 0; k <= S; k++) {
sum += 1ull * a[i][k] * tr[j][k];
if ((k & 15) == 15) sum %= MOD;
}
tmp.a[i][j] = sum % MOD;
}
}
return tmp;
}
};
int L(int k) {
return k == 0 ? 0 : k;
}
int R(int k) {
return k == m + 1 ? 0 : m + k;
}
int ANS() {
return 2 * m + 1;
}
matrix c;
void dd(int& a) {
a = dec(a, 1);
}
matrix qpow(matrix b, int p) {
matrix res;
for (int i = 0; i <= S; i++) res[i][i] = 1;
while (p) {
if (p & 1) res = res * b;
b = b * b;
p >>= 1;
}
return res;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m >> MOD;
int T = L(m);
for (int i = 0; i <= 301; i++) {
for (int j = 0; j <= 301; j++) c[i][j] = 0;
}
S = 2 * m + 1;
for (int k = 1; k <= m; k++) {
for (int r = 1; r <= k; r++) {
for (int l = 1; l <= r; l++) {
c[L(m)][L(k)] = inc(c[L(m)][L(k)], 1);
dd(c[L(l - 1)][L(k)]);
dd(c[R(r + 1)][L(k)]);
}
}
for (int r = k; r <= m; r++) {
for (int l = k; l <= r; l++) {
c[L(m)][R(k)] = inc(c[L(m)][R(k)], 1);
dd(c[L(l - 1)][R(k)]);
dd(c[R(r + 1)][R(k)]);
}
}
}
for (int i = 0; i <= S; i++) {
c[i][ANS()] = c[i][L(m)];
}
c[ANS()][ANS()] = inc(c[ANS()][ANS()], 1);
matrix ans;
for (int k = 1; k <= m; k++) ans[0][L(k)] = k % MOD;
ans[0][R(1)] = m % MOD;
ans[0][ANS()] = m % MOD;
if (n > 1) {
c = qpow(c, n - 1);
ans = ans * c;
}
cout << ans[0][ANS()] << endl;
return 0;
}

浙公网安备 33010602011771号