2022牛客冬令营 第六场 题解
A题 回文大师
B题 价值序列 (数学)
对于一个长度为 \(n\) 的正整数数列 \(\{a_n\}\),记数列价值为 \(\sum\limits_{i=1}^{n-1}|a_i-a_{i+1}|\)。数组长度为 1 时,价值为 0。
问,这个数列有多少个子序列(不要求连续),其价值和原价值相同?
\(1\leq n\leq 10^5,1\leq a_i\leq 10^9\)
有绝对值不等式 \(|a|+|b|\geq |a+b|\),这是本题的解题关键。
对于连续的三个数 \(a,b,c\),那么有 \(|a-b|+|b-c|\geq|(a-b)+(b-c)|=|a-c|\),当且仅当 \(a\leq b\leq c\) 或 \(a\geq b\geq c\) 时取等。
那么,当某个值满足上面这个条件时,这个数就是可省略的,反之则不行。(显然,头尾两个数肯定没啥省略)
针对各不相同的情况,上面的讨论已经足够,不过当有些数连续重复出现时,我们需要再单独标记下,例如某个数连续出现了 \(k\) 次,那么如果这个数不可省略,那么答案就要乘上 \(2^k-1\),反之则乘上 \(2^k\)(例如 \([1,5,5,5,4]\) 和 \([1,4,4,4,5]\))。
详情请见代码:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
const LL mod = 998244353;
LL power(LL a, LL b) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
int n, m, a[N], b[N], s[N];
LL merge(int len) {
return (power(2, len) - 1 + mod) % mod;
}
bool check(int x, int y, int z) {
return (x <= y && y <= z) || (x >= y && y >= z);
}
LL solve() {
//read
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//merge
int L = 1;
m = 0;
while (L <= n) {
int R = L;
while (a[R] == a[L] && R <= n) R++;
b[++m] = a[L], s[m] = R - L;
L = R;
}
//solve
if (m == 1) return merge(n);
LL ans = merge(s[1]) * merge(s[m]) % mod;
for (int i = 2; i < m; ++i)
if (check(b[i - 1], b[i], b[i + 1]))
ans = ans * power(2, s[i]) % mod;
else
ans = ans * merge(s[i]) % mod;
return ans;
}
int main()
{
int T;
scanf("%d", &T);
while (T--) printf("%lld\n", solve());
return 0;
}
C题 数组划分
D题 删除子序列 (贪心)
给定一个字符串 \(s\) 和另一个串 \(t\),问我们至多可以从字符串 \(s\) 中删除多少个子串 \(t\)?
全部小写字母,\(1\leq |s|\leq 10^6,1\leq |t|\leq 26\),保证 \(t\) 中字符各不相同
直接贪心,每次尽量从后面删,删不了了则返回答案。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, m;
char s[N], t[N];
inline int id(char c) { return c - 'a'; }
stack<int> q[26];
int solve()
{
//init
for (int i = 0; i < 26; ++i)
while (!q[i].empty()) q[i].pop();
//read
scanf("%d%d%s%s", &n, &m, s + 1, t + 1);
//solve
for (int i = 1; i <= n; ++i)
q[id(s[i])].push(i);
int res = 0;
while (true) {
int p = n + 1;
for (int i = m; i >= 1; i--) {
int c = id(t[i]);
while (!q[c].empty() && q[c].top() >= p) q[c].pop();
if (q[c].empty()) return res;
p = q[c].top(); q[c].pop();
}
res++;
}
}
int main()
{
int T;
scanf("%d", &T);
while (T--) printf("%d\n", solve());
return 0;
}
E题 骑士(贪心/排序)
现在有 \(n\) 个骑士,第 \(i\) 个骑士的攻击力为 \(a_i\),防御为 \(b_i\),血量为 \(h_i\)。如何两个骑士 \(i,j\),满足 \(a_i-b_j\geq h_j\),那么骑士 i 就可以秒杀 j。
现在大家不希望自己能被除了自己外的其他骑士秒杀,于是去商店买药水(每瓶药水可以让自己血量上升 1),问至少需要多少瓶药水?
\(1\leq n\leq 2*10^5,1\leq a_i,b_i,h_i\leq 10^9\)
记 \(c_i=b_i+h_i\) 为骑士的血量更为合适,然后按照攻击力对骑士来排序,前 \(n-1\) 个骑士看看伤害最高的骑士的伤害和自己血量的差,补足即可,而伤害最高的骑士则去看第 \(i-1\) 个骑士。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 200010;
int n;
struct Node {
LL a, b;
bool operator < (const Node &rhs) const {
return a < rhs.a;
}
} t[N];
LL a[N], b[N];
LL solve()
{
//
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
LL A, P, H;
scanf("%lld%lld%lld", &A, &P, &H);
t[i] = (Node){A, P + H};
}
//
sort(t + 1, t + n + 1);
for (int i = 1; i <= n; ++i)
a[i] = t[i].a, b[i] = t[i].b;
//
LL res = 0;
for (int i = 1; i <= n; ++i)
if (i < n)
res += max(a[n] - b[i] + 1, 0LL);
else res += max(a[n - 1] - b[i] + 1, 0LL);
return res;
}
int main()
{
int T;
scanf("%d", &T);
while (T--) printf("%lld\n", solve());
return 0;
}
F题 +-串 (贪心)
给定一个长度为 \(n\) 的+-串,记 \(f\) 为串中 +号数量和 -号数量的差的绝对值。
现在我们可以进行 \(k\) 次操作,每次将串中的某个符号取反,问操作完成后 \(f\) 最小可以变成多少。
\(1\leq k\leq n\)
显然,我们每次操作可以使得 \(f\) 上升或者下降 2,或者不变(从 \(|1|\) 变为 \(|-1|\))。
那么,我们尽可能往下降,降到 1 了则输出,降到 0 了看看剩下来还剩多少次来确定答案是 0 还是 2,降不到的话则输出能达到的最小值。
#include<bits/stdc++.h>
using namespace std;
int n, k;
string s;
int solve() {
cin >> s >> k;
int x = 0;
for (char c : s)
c == '+' ? x++ : x--;
x = abs(x);
if (x > 2 * k) return x - 2 * k;
else if (x % 2 == 1) return 1;
else return ((k - x / 2) % 2 == 0) ? 0 : 2;
}
int main()
{
int T;
cin >> T;
while (T--) printf("%d\n", solve());
return 0;
}
G题 迷宫2 (01 BFS)
给定一个 \(n\) 行 \(m\) 列的迷宫,每个格子上面有一个字符(UDLR),小红会按照符号的表示来走。起点在 (1, 1) 处,终点在 (n, m) 处。
不过,这样是不一定能到达终点的,所以小明打算在小红开始走之前先预先改一下部分格子的字符。问至少需要修改多少格子,可以使得小红走到终点?
\(q\leq n,m\leq 10^3\)
《算法竞赛进阶指南》P116 上面有一个类似的题目:电路维修 (题面有所出入,不过都是一个题目)。
我们考虑一个点向四周走,倘若它走向按照字符应该走的那个格子,那么距离不变,反之增加 1,那么我们就相当于在一个边权仅为 0 和 1 的图上走最短路,可以使用 01 BFS 来解决(想用最短路也行),复杂度 \(O(nm)\)。
(好家伙,以前没实际上手写过双端队列DFS,没想到今天给 debug 了两个小时,麻了)
#include<bits/stdc++.h>
using namespace std;
const int N = 1010, INF = 1e9 + 10;
int n, m;
char a[N][N];
inline bool can(int x, int y) {
return x > 0 && x <= n && y > 0 && y <= m;
}
const int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
const char dc[4] = {'D', 'R', 'U', 'L'};
int dis[N][N], vis[N][N];
deque< pair<int, int> > q;
struct Node {
int x, y;
char ch;
} pre[N][N];
void solve() {
//read
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", a[i] + 1);
//init
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
dis[i][j] = INF, vis[i][j] = 0;
while (!q.empty()) q.pop_back();
//BFS
dis[1][1] = 0, pre[1][1] = (Node){0, 0, 0};
q.push_back(make_pair(1, 1));
while (!q.empty()) {
int x = q.front().first,
y = q.front().second;
q.pop_front();
if (vis[x][y]) continue;
vis[x][y] = 1;
for (int i = 0; i < 4; ++i) {
int tx = x + dx[i], ty = y + dy[i];
if (!can(tx, ty)) continue;
if (a[x][y] == dc[i]) {
if (dis[x][y] < dis[tx][ty]) {
dis[tx][ty] = dis[x][y];
pre[tx][ty] = (Node){x, y, dc[i]};
if (!vis[tx][ty])
q.push_front(make_pair(tx, ty));
}
}
else {
if (dis[x][y] + 1 < dis[tx][ty]) {
dis[tx][ty] = dis[x][y] + 1;
pre[tx][ty] = (Node){x, y, dc[i]};
if (!vis[tx][ty])
q.push_back(make_pair(tx, ty));
}
}
}
}
//output
printf("%d\n", dis[n][m]);
int x = n, y = m;
while (x) {
Node t = pre[x][y];
x = t.x, y = t.y;
if (a[x][y] != t.ch)
printf("%d %d %c\n", t.x, t.y, t.ch);
}
}
int main()
{
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}
H题 寒冬信使2 (状压,SG博弈)
有 \(T\) 组数据。(\(1\leq T \leq 3000\))
给定 \(n\) 个排成一排的格子,从左到右记为 1 到 n,每个格子的颜色为黑或者白。A 和 B轮番进行游戏,A先手,每当轮到一个人时,他可以选择进行某个操作:
- 选择两个整数 \(1\leq i<j\leq n\)(要求第 i 个格子必须白色),然后将他们分别反转颜色(原来白色就变成黑色,反之亦然)
- 当第一个格子是白色的时候,可以选择将其反转为黑色
当某个人无法再进行操作时候(例如所有各种都是黑色),那么他就输了。问在两人均采取最优策略下,A是否先手必胜?
\(1\leq n\leq 10\)
将每种状态变为一个 \(n\) 位的二进制数(黑 0 白 1,状态压缩),然后就转变为了基于 SG 函数的博弈论问题(其实就是半个 DP),预处理的复杂度为 \(O(n^22^n)\),边界条件为 \(dp_0=0\)。
预处理好之后,对于每组询问,直接 \(O(1)\) 回答即可。(注意到,一个状态的必胜或者必败仅和本身有关,与场上一共有多少个格子没关系,所以只要针对 \(n=10\) 预处理一遍即可)。
#include<bits/stdc++.h>
using namespace std;
int dp[1024];
int dfs(int x) {
if (dp[x]) return dp[x];
bitset<10> s = x;
if (s[0] == 1 && dfs(x ^ 1) == -1) return dp[x] = 1;
for (int j = 1; j < 10; ++j)
if (s[j] == 1)
for (int i = 0; i < j; ++i) {
int y = x ^ (1 << i) ^ (1 << j);
if (dfs(y) == -1) return dp[x] = 1;
}
return dp[x] = -1;
}
int main()
{
dp[0] = -1;
int T;
cin >> T;
while (T--) {
int n;
string s;
cin >> n >> s;
int x = 0;
for (int i = n - 1; i >= 0; --i)
x = x * 2 + (s[i] == 'w');
puts(dfs(x) == 1 ? "Yes" : "No");
}
return 0;
}
I题 A+B问题 (模拟)
在 k 进制下求 \(A+B\) 的值。
\(2\leq k\leq 10,A,B>0,|A|,|B|\leq 2*10^5\)
直接硬模拟即可,作为签到题算是比较搞心态的。
#include<bits/stdc++.h>
using namespace std;
deque<int> a, b, res;
int main()
{
int k;
string s1, s2;
cin >> k >> s1 >> s2;
for (char ch : s1) a.push_front(ch - '0');
for (char ch : s2) b.push_front(ch - '0');
//补齐位数
while (a.size() < b.size()) a.push_back(0);
while (b.size() < a.size()) b.push_back(0);
a.push_back(0); b.push_back(0);
//模拟竖式计算
int c = 0;
while (!a.empty()) {
int x = a.front() + b.front() + c;
a.pop_front(); b.pop_front();
res.push_front(x % k);
c = x / k;
}
//除去前导0并输出
while (res.front() == 0 && !res.empty())
res.pop_front();
if (res.empty()) cout << 0;
else for (int x : res) cout << x;
return 0;
}
J题 牛妹的数学难题 (组合数学)
给定长度为 \(n\) 的正整数数列 \(\{a_n\}\) 和整数 \(k\),计算
\[\sum\limits_{i_1=1}^{n}\sum\limits_{i_2=i_1+1}^{n}\cdots\sum\limits_{i_k=i_{k-1}+1}^{n}(\prod\limits_{i=1}^ka_i) \]的值。
\(1\leq k\leq n\leq 10^7,0\leq a_i\leq 2\)
这题本质上就是求出各种不同组合下的乘积之和(可以让 \(k=2,3\) 试试看)
发现了 \(a_i\) 的极小范围后,我们直接将其分成两类(1 和 2,0 对答案无贡献),记 1 的数量为 \(a\),2 的数量为 \(b\),那么就变成了组合求和的问题(这个组合数必须得 \(O(1)\) 的求出来)。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL mod = 998244353;
const int N = 10000010;
LL fact[N << 1], inv[N << 1];
//快速幂
LL power(LL a, LL b) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
//处理n!的逆元
void init(int n) {
fact[0] = 1;
for (int i = 1; i <= n; ++i)
fact[i] = fact[i - 1] * i % mod;
inv[n] = power(fact[n], mod - 2);
for (int i = n - 1; i >= 0; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
}
LL C(LL n, LL m) {
if (m > n) return 0;
return fact[n] * inv[m] % mod * inv[n - m] % mod;
}
//
int n, k;
int main()
{
init(10000000);
//read
scanf("%lld%lld", &n, &k);
int cnt[3];
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
cnt[x]++;
}
//solve
LL ans = 0;
int a = cnt[1], b = cnt[2];
for (int x = min(a, k), y = k - x; x >= 0 && y <= b; --x, ++y)
ans = (ans + C(a, x) * C(b, y) % mod * power(2, y) % mod) % mod;
cout << ans;
return 0;
}