CF Round 785 Div2 题解

比赛链接

A题 Subtle Substring Subtraction(贪心)

给定一个长度为 \(n\) 的字符串,两个人 \(A,B\) 轮流进行游戏。

每当轮到 \(A\) 时,他可以从现在的小写字母字符串中取走一个长度为偶数的子串并得到对应分数,而 \(B\) 则只能取走长度为奇数的子串。

两个人均采取最优策略行动,问最后字符串为空的时候,谁的得分最高,比另外一个人高多少分?

一个字符串的对应分数定义:\(a\) 对应 \(1\)\(b\) 对应 \(2\),以此内推,字符串的分数就是每个字符对应分数之和(例如 \(f(aabca)=1+1+2+3+1=8\)

\(|s|\leq 2*10^5\)

\(n=1\) 时,显然 \(A\) 无法操作,然后 \(B\) 拿走,\(B\) 取胜。

\(n\) 为偶数时,直接让 \(A\) 拿走所有字符即可,\(A\) 取胜。

\(n\) 为奇数时,最好的方法就是拿走一个 \(n-1\) 长度的子串(这种方案得分最优),且 \(A\) 必胜(如果存在着 \(B\) 拿一个字符的分数超过那 \(n-1\) 的分数的情况,例如 \(aaaz\),那 \(A\) 可以选择去拿包含 \(z\) 的那个子串),只要判断一手拿 \([1,n-1]\) 还是 \([2,n]\) 即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n;
char s[N];
int f(char ch) { return ch - 'a' + 1; }
int calc(int l, int r) {
    int res = 0;
    for (int i = l; i <= r; ++i) res += f(s[i]);
    return res;
}
void solve() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    if (n == 1)
        cout << "Bob " << calc(1, 1) << endl;
    else if (n % 2 == 0)
        cout << "Alice " << calc(1, n) << endl;
    else
        cout << "Alice " << max(calc(1, n - 1) - f(s[n]), calc(2, n) - f(s[1])) << endl;
}
int main()
{
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

B题 A Perfectly Balanced String? (枚举+前缀和 / 思维)

给定一个小写字母字符串 \(s\),问对于任意一个三元组 \((t,u,v)\)\(t\)\(s\) 的子串,\(u,v\) 都是 \(s\) 中的字符),\(u\) 的出现次数和 \(v\) 的出现次数之差都小于等于 1?

\(|s|\leq 2*10^5\)

方法1:枚举+前缀和

我们简化问题:给定字符 \(a,b\),是否 \(s\) 的所有子串都满足该性质?

我们考虑做一次前缀和(\(A_i,B_i\) 分别表示区间 \([1,i]\)\(a,b\) 出现的次数),那么一旦出现不满足性质的情况,就说明存在 \(1\leq l\leq r\leq n\),满足 \((A_r-A_{l-1})-(B_r-B_{l-1})>1\) 或者 \(<-1\)

我们构造 \(D_i=A_i-B_i(D_0=0)\),那么不满足性质的情况就变为了 \(D_r-D_{l-1}>1\)\(D_r-D_{l-1}<-1\)。换言之,我们直接在 \(D\) 数组中找两个绝对值相差超过 \(1\) 的元素即可,直接排序后比较 \(D_n\)\(D_0\) 就搞定了。

那么,扩展到本题,因为小写字母一共只有 26 个,所以可以直接枚举所有可能,最坏复杂度为 \(O(26^2n\log n)\)。(这个居然能过,我怀疑换个评测机就 G 了)

(其实没必要这么赶,找最大最小值只需要 \(O(n)\) 遍历一遍就行了)

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
char s[N];
int n, a[N], b[N], d[N];
bool check(char c1, char c2) {
    for (int i = 1; i <= n; ++i) {
        a[i] = a[i - 1] + (s[i] == c1);
        b[i] = b[i - 1] + (s[i] == c2);
        d[i] = a[i] - b[i];
    }
    int Min = 1e9, Max = -1;
    for (int i = 0; i <= n; ++i)
        Min = min(Min, d[i]), Max = max(Max, d[i]);
    return Max - Min <= 1;
}
bool solve() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    unordered_map<char, int> vis;
    for (int i = 1; i <= n; ++i) vis[s[i]] = 1;
    for (int c1 = 'a'; c1 < 'z'; ++c1)
        for (int c2 = c1 + 1; c2 <= 'z'; ++c2)
            if (vis[c1] && vis[c2] && !check(c1, c2)) return false;
    return true;
}
int main()
{
    int T;
    cin >> T;
    while (T--) puts(solve() ? "YES" : "NO");
    return 0;
}

方法2:思维+结论

当且仅当字符串具有周期性,且一个循环节内所有字符各不相同。

具体证明可见题解,这里给出另外一个 julao 的解法:

  1. 假定字符串中一共有 \(m\) 种不同的字符,那么 \([1.m]\) 上必然包含了全部字符(且互不相等)
  2. 移动到 \([2,m+1]\) 上同理,随后不断移动,证明周期性

C题 Palindrome Basis(完全背包)

任意一个正整数,都可以表示为若干个回文数之和,并存在多种方案(样例可见原题)。

\(T\) 组询问,每次给定一个正整数 \(x\),求 \(x\) 的表示方案。

\(T\leq 10^4,x\leq 4*10^4\)

我们记 \(n=4*10^4\),显然,我们可以先 \(O(n)\) 枚举出所有范围内的回文数(总数为 \(m=500\) 左右),随后跑完全背包即可,复杂度 \(O(nm)\)

不过,普通完全背包的转移方式(先容量 j 后物品 i)会导致重复,所以我们必须得先 i 后 j,当枚举到物品 i 的时候,\(dp_j\) 就表示的是在仅使用前 \(i-1\) 个物品时数字 \(j\) 的表示方式,这样就可以做到不重复。

#include<bits/stdc++.h>
using namespace std;
bool check(int x) {
    int tmp[10], n = 0;
    for (; x; x /= 10) tmp[++n] = x % 10;
    for (int i = 1; i <= n; ++i)
        if (tmp[i] != tmp[n + 1 - i]) return false;
    return true;
}
vector<int> vec;
const int mod = 1e9 + 7;
const int N = 40010;
int dp[N];
int main()
{
    int n = 40000;
    for (int i = 1; i <= n; ++i)
        if (check(i)) vec.push_back(i);
    //init
    dp[0] = 1;
    for (int v : vec)
        for (int i = v; i <= n; ++i)
            dp[i] = (dp[i] + dp[i - v]) % mod;
    //query
    int T;
    cin >> T;
    while (T--) {
        int x;
        cin >> x;
        cout << dp[x] << endl;
    }
    return 0;
}

(本题原型是自然数拆分,也就是《算法竞赛进阶指南》上面背包章节的这一题:自然数拆分Lunatic版

D题 Lost Arithmetic Progression(数学)

给定两个有限项等差数列 \(A,B\)(给定首项,公差和项数),他们的公共项构成新数列 \(C\)(显然,\(C\) 也是一个等差数列)。

现在给定数列 \(B,C\),问有多少符合要求的数列 \(A\)?(无穷则输出 -1)

\(T\leq 100\),每组数据中,首项在 int 范围内,公差为 int 内正整数,项数在 \(int\) 内且大于 1

无解的情况,就是 C 并不属于 B 的一部分,这就直接判断了:

  1. 首项是否在 B 内
  2. C 的公差是不是 B 的公差的倍数
  3. C 的最大项是不是在 B 内

接下来就是枚举 \(A\) 的可能公差,外加它在 C 的左右还能延续多少。

  1. 枚举公差

    新公差所构造出来的数列应该避免和 B 有其他交点。

    我们记 C 公差为 dc,B 公差为 db,那么 db 将 dc 分成了 \(\frac{dc}{db}\) 份。那么,我们必须保证 \(\gcd(\frac{dc}{db},\frac{dc}{da})=1\)

  2. 左右延伸

    处理完了内部,我们记 C 的上下界分别为 CL,CR,那么左右之多延伸到 CL - DC, CR + DC 处,但如果这两个里面有一个不在 B 范围内,就可以无穷枚举下去(无穷解),反之则有限。

这个 D 就是这样一个纯数学加分类讨论,每组数据的复杂度为 \(O(\sqrt{10^9})\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL mod = 1e9 + 7;
LL gcd(LL a, LL b) {
    return !b ? a : gcd(b, a % b);
}
LL solve() {
    LL sb, db, nb, sc, dc, nc;
    cin >> sb >> db >> nb >> sc >> dc >> nc;
    //no solution
    if (dc % db != 0) return 0;
    LL BL = sb, BR = sb + (nb - 1) * db;
    LL CL = sc, CR = sc + (nc - 1) * dc;
    if (CL < BL || BR < CR) return 0;
    if (abs(sc - sb) % db != 0) return 0;
    //try
    vector<LL> vec;
    for (LL i = 1; i * i <= dc; ++i)
        if (dc % i == 0) {
            vec.push_back(i);
            if (dc != i * i) vec.push_back(dc / i);
        }
    LL bdiv = dc / db, ans = 0;
    for (LL x : vec) {
        LL adiv = dc / x;
        if (gcd(adiv, bdiv) != 1) continue;
        //inf
        if (CR + dc > BR || CL - dc < BL) return -1;
        LL k = adiv % mod;
        ans = (ans + k * k) % mod;
    }
    return ans % mod;
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}
posted @ 2022-05-01 14:01  cyhforlight  阅读(45)  评论(0编辑  收藏  举报