Codeforces Round 927 (Div. 3)
Codeforces Round 927 (Div. 3)
晚打了将近 40min。但还是太菜了。
C. LR-remainders
Description
给定一个长度为 \(n\) 的数组 \(a\) 和 \(n\) 个指令,每条指令为 \(\texttt{L,R}\) 中的一种。
依次处理每个指令:
- 首先,输出 \(a\) 中所有元素的乘积除以 \(m\) 的余数。
- 然后,如果当前指令为 \(\texttt L\),则移除数组 \(a\) 中最左边的元素;如果当前指令为 \(\texttt R\),则移除数组 \(a\) 中最右边的元素。
Solution
首先不难发现每次操作是求一段区间的乘积模 \(m\)。不难想到用数据结构(比如线段树、ST 表)维护。注意如果维护前缀积是不合法的,因为有可能 \(s_{l - 1} = 0\),除法就假了。
数据结构维护的复杂度是 \(\mathcal O(n \log n)\) 的。因为不想写线段树所以写了个 \(\mathcal O(n)\) 的做法。
我们可以倒序处理每个操作,原来的删除操作转化成加数操作。这样我们维护的答案每次操作会将它乘上一个数而非除以一个数,这样就避免了取模后为 \(0\) 的数。
具体的,我们维护当前剩余的没有被删除的数构成的区间 \([l, r]\),并维护当前答案。初始时按照读入顺序求出 \(l, r\) 所在位置,并令答案为 \(1\)。
倒叙处理每一个操作:
- 如果当前操作为 \(\texttt L\),将 \(l \gets l - 1\),将答案乘上 \(a_l\);
- 如果当前操作为 \(R\),将 \(r \gets r + 1\),将答案乘上 \(a_r\);
然后再倒叙输出答案即可。
Code
void solve() {
int n = read(), m = read();
vector<int> a(n), res(n, 0);
res[0] = 1;
for (int &t : a) t = read(), (res[0] *= t) %= m;
int l = 0, r = n - 1;
string s = "";
for (int i = 0; i < n; ++ i ) {
char c;
cin >> c;
if (c == 'L') ++ l;
else -- r;
s.push_back(c);
}
int x = 1;
for (int i = n - 1; i; -- i ) {
if (s[i] == 'L') {
-- l;
(x *= a[l]) %= m;
}
else {
++ r;
(x *= a[r]) %= m;
}
res[i] = x;
}
for (int &t : res) wsp(t);
}
D. Card Game
Description
两名玩家正在玩一款在线纸牌游戏。游戏使用一副 \(32\) 张牌。每张牌都有花色和等级。共有四种花色:梅花、方块、红心和黑桃。我们将分别用字符 "C"、"D"、"H "和 "S "对它们进行编码。共有 \(8\) 个等级,依次递增:'2', '3', '4', '5', '6', '7', '8', '9'.
每张牌都用两个字母表示:等级和花色。例如,红心 \(8\) 表示为 \(8H\)。
游戏开始时,选择一种花色作为王牌花色。
在每一轮游戏中,玩家都要这样出牌:第一位玩家将自己的一张牌放在桌上,第二位玩家必须用自己的一张牌击败这张牌。之后,两张牌都被移至弃牌堆。
如果两张牌的花色相同,且第一张牌的等级高于第二张牌,那么这张牌就能打败另一张牌。例如,\(8S\) 可以打败 \(4S\)。例如,如果王牌花色是梅花("C"),那么 \(3C\) 可以击败 \(9D\)。请注意,王牌只能被等级更高的王牌击败。
游戏中一共进行了 \(n\) 轮,因此弃牌堆中现在有 \(2n\) 张牌。你想重建游戏中的回合,但是弃牌堆中的牌是洗过的。请找出游戏中可能出现的 \(n\) 个回合。
Solution
首先我们将非王牌花色的每种花色两两配对。
此时可能会剩下一些牌,再把这些牌和王牌花色配对。
最后把剩下的王牌花色两两配对。
在模拟上述过程时判断是否无解即可。
Code
map<char, int> mp({{'S', 0}, {'C', 1}, {'H', 2}, {'D', 3}});
map<int, char> pm({{0, 'S'}, {1, 'C'}, {2, 'H'}, {3, 'D'}});
void solve() {
int n = read();
char c; cin >> c;
int BIG = mp[c];
vector<int> a[4] = {{}, {}, {}, {}};
for (int i = 1; i <= 2 * n; ++ i ) {
string str; cin >> str;
a[mp[str[1]]].push_back(str[0] - '0');
}
fup (i, 0, 3) {
if (a[i].size()) sort(a[i].begin(), a[i].end());
}
int k = 0;
fup (i, 0, 3)
if (i != BIG) k += a[i].size() % 2;
if (a[BIG].size() >= k && (a[BIG].size() - k) % 2 == 0) {
fup (i, 0, 3) if (i != BIG) {
int len = a[i].size();
for (int j = 0; j + 1 < len; j += 2 ) {
cout << a[i][j] << pm[i] << ' ' << a[i][j + 1] << pm[i] << '\n';
}
if (len % 2) {
cout << a[i].back() << pm[i] << ' ' << a[BIG].back() << c << '\n';
a[BIG].pop_back();
}
}
int x = a[BIG].size();
for (int i = 0; i + 1 < x; i += 2 ) {
cout << a[BIG][i] << c << ' ' << a[BIG][i + 1] << c << '\n';
}
}
else puts("IMPOSSIBLE");
}
E. Final Countdown
Description
有一个钟表,当前显示的时间是一个 \(n\) 位数。
从当前时刻变道下一个时刻需要花费的时间为发生变化的数位数量。
例如,从 \(3200\) 变到 \(3199\) 需要花费 \(3\) 个单位时间。
求变到 \(0\) 的时间。
Solution
首先可以开贡献做。例如 \(12345\):
- 最高位 \(1\) 只会变化 \(1\) 次,即 \(10000 \to 09999\) 时变。
- 第二位 \(2\) 会变化 \(12\) 次,即 \(12000 \to 11999\),\(11000 \to 10009\),\(10000 \to 09999\),\(09000 \to 08999\),\(08000 \to 07999\),\(07000 \to 06999\),\(06000 \to 05999\),\(05000 \to 04999\),\(04000 \to 03999\),\(03000 \to 02999\),\(02000 \to 01999\),\(01000 \to 00999\) 时变。
- 第三位 \(3\) 会变化 \(123\) 次;
- 第四位 \(4\) 会变化 \(1234\) 次;
- 第五位 \(5\) 会变化 \(12345\) 次。
不难发现就是原数的每个前缀的和。例如 \(12345\) 的答案为 \(1 + 12 + 123 + 1234 + 12345\)。
再拆贡献,还是 \(1 + 12 + 123 + 1234 + 12345\) 的例子:
-
最高位 \(1\) 的贡献次数为 \(1 + 10 + 10^2 + 10^3 + 10^4 = 11111\),贡献为 \(11111 \times 1 = 11111\);
-
第二位 \(2\) 的贡献次数为 \(1 + 10 + 10^2 + 10^3 = 1111\),贡献为 \(1111 \times 2 = 2222\);
-
第三位 \(3\) 的贡献次数为 \(1 + 10 + 10^2 = 111\),贡献为 \(111 \times 3 = 333\);
-
第四位 \(4\) 的贡献次数为 \(1 + 10 = 11\),贡献为 \(11 \times 2 = 22\);
-
第五位 \(5\) 的贡献次数为 \(1\),贡献为 \(1 \times 5\)。
所以总答案为 \(11111 + 2222 + 333 + 44 + 5 = 13715\)。
列竖式就是:
5
44
333
2222
11111
-----
13715
所以可以按位计算:
- 个位上是原数的数位和,即 \(1 + 2 + 3 + 4 + 5 = 15\);
- 十位上是原数的前 \(4\) 位和,即 \(1 + 2 + 3 + 4 = 10\);
- 百位上是原数的前 \(3\) 位和,即 \(1 + 2 + 3 = 6\);
- 千位上是原数的前 \(2\) 位和,即 \(1 + 2 = 3\);
- 万位上是原数的前 \(1\) 位和,即 \(1\)。
所以可以维护前缀和(实际上按照数位反转后就是后缀和)计算答案的每一位,然后处理进位即可。
Code
void solve() {
int n = read(), suf = 0;
vector<int> a(n), res(n + 1, 0);
fup (i, 0, n - 1) {
char c;
cin >> c;
a[i] = c - '0';
suf += a[i];
}
reverse(a.begin(), a.end());
fup (i, 0, n - 1) {
res[i] = suf;
suf -= a[i];
}
int fin = n;
fup (i, 0, n - 1) {
res[i + 1] += res[i] / 10;
res[i] %= 10;
}
while (!res[fin] && fin) fin -- ;
fdw (i, fin, 0) cout << res[i];
puts("");
}
F. Feed Cats
Desciption
数轴上有 \(n\) 个点和 \(m\) 条线段 \([l_i, r_i]\)。
你会删掉若干条线段。接下来你需要在 \(1 \sim n\) 中选择几个整数点,使得剩下的线段中没有一条线段覆盖两个及以上选中点。
求最多的剩下的线段数量。
\(1 \le n \le 10^6\),\(1 \le m \le 2 \times 10^5\)。
Solution
设 \(f_i\) 表示如果只在 \(1 \sim i\) 中选点,最多能剩下几条线段。转移枚举点 \(i\) 选与不选,有:
其中 \(L_i\) 表示所有包含点 \(i\) 的线段中左端点的最小值,\(V_i\) 表示有多少条线段覆盖了点 \(i\)。
这样做的原因是,如果我们选择了点 \(i\),那么所有覆盖 \(i\) 的线段所覆盖的其它点我们就都不能选了。所以记录最靠左的这个点 \(L_i\),从 \(f_{L_i - 1}\) 转移过来即可。
所以我们只需要预处理 \(L_i, V_i\)。
每添加一条线段 \([l_i, r_i]\),相当于所有的 \(V_i(l \le i \le r)\) 都加一。先维护差分数组最后再求一遍前缀和即可。
\(L_i\) 可以用递推的方式来求。初始化所有 \(L_i \gets \infty\)。接下来对于一条线段 \([l_i, r_i]\),很显然有 \(L_{r_i} = l_i\)。然后递推 \(L_i \gets \min(L_i, L_{i + 1})\)。
最后答案为 \(f_n\)。
Code
int lft[N], f[N], s[N];
int solve() {
int n = read(), m = read();
fup (i, 0, n + 1) {
lft[i] = N;
f[i] = s[i] = 0;
}
fup (i, 1, m) {
int l = read(), r = read();
++ s[l], -- s[r + 1];
lft[r] = min(lft[r], l);
}
fdw (i, n - 1, 0) lft[i] = min(lft[i], lft[i + 1]);
db(lft + 1, lft + n + 1);
fup (i, 1, n) s[i] += s[i - 1];
fup (i, 1, n) {
if (lft[i] == N) f[i] = f[i - 1] + s[i];
else f[i] = max(f[i - 1], f[lft[i] - 1] + s[i]);
}
return f[n];
}