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. 选择两个整数 \(1\leq i<j\leq n\)(要求第 i 个格子必须白色),然后将他们分别反转颜色(原来白色就变成黑色,反之亦然)
  2. 当第一个格子是白色的时候,可以选择将其反转为黑色

当某个人无法再进行操作时候(例如所有各种都是黑色),那么他就输了。问在两人均采取最优策略下,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;
}
posted @ 2022-02-16 15:00  cyhforlight  阅读(41)  评论(0编辑  收藏  举报