牛客周赛 Round 72 题解

牛客周赛 Round 72 题解

A 小红的01串(一)

直接遍历即可

#include <bits/stdc++.h>
using namespace std;

void solve() {
	string s; cin >> s;
    int n = s.size();
    int cnt = 0;
    for (int i = 1; i < n; i ++ ) {
        if (s[i] != s[i - 1]) cnt ++;
    }
    cout << cnt << endl;
}

int main() {
	int T = 1;
// 	cin >> T;
	while (T -- )
		solve();
	return 0;
} 

B 小红的01串(二)

可以看出,所有满足的串一定是 0101.. 或者 1010... 这样串的子串,所以我们找出这些串,对于长度为 \(m\) 的串,它们的所有长度为2的连续子串的个数为 \(1+2+...+m-1\),高斯公式统计即可。记得开 long long

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

void solve() {
	string s; cin >> s;
    int n = s.size();
    LL ans = 0;
    for (int i = 0; i < n; i ++ ) {
        int j = i + 1;
        while (j < n && s[j] != s[j - 1]) j ++;
        int len = j - i;
        ans += 1ll * len * (len - 1) / 2;
        i = j - 1;
    }
    cout << ans << endl;
}

int main() {
	int T = 1;
// 	cin >> T;
	while (T -- )
		solve();
	return 0;
} 

C 小红的01串(三)

首先考虑 \(-1\) 的情况。

  • 如果 \(k=0\) ,那么 \(a, b\) 中也得有一个为 \(0\) ,否则就输出 \(-1\)
  • 如果 \(k\gt 0\) ,那么需要对 \(a, b\) 分类讨论
    • \(a = b\) ,那么 \(k\) 的理论最大值是 \(a + b - 1\) ,如果超出这个范围就是 \(-1\)
    • \(a \ne b\) ,那么 \(k\) 的理论最大值是 \(2 \times \min(a, b)\) ,如果超出这个范围就是 \(-1\)

接下来考虑构造。通过刚刚判断 \(-1\) 的过程中可以看出,我们可以先通过 \(0101...\) 这样的方式凑出 \(k\) 来,多出来的 \(0\) 或者 \(1\) 找某个地方一起插进去即可。

如果直接写就会有一些问题:我们不知道是 \(0\) 开始构造还是 \(1\) 开始,需要写多个if,感觉很麻烦。

所以我们可以把这种操作封装成一个函数,具体看代码。

#include <bits/stdc++.h>
using namespace std;

int k;

void sort_01(int a, int b, int n1, int n2) {
    // 其中n1,n2一个为0一个为1,表示我们要填的数
    // 表示当前有a个n1和b个n2要填,其中我们想拿n1填第一个数
    int numa = k / 2 + 1;    // 要填numa个n1
    int numb = (k + 1) / 2;  // 要填numb个n2
    for (int i = 1; i <= a - numa; i ++ ) cout << n1; // 先将多余的n1输出
    cout << n1; // 输出主体部分中第一个n1
    if (k & 1) {
        // 如果是k是奇数,那么一定是n1开头n2结束,所以可以放心的把多余的n2放到尾部
        for (int i = 1; i <= k; i ++ ) {
            // 将n2和n1交替输出
            if (i & 1) cout << n2;
            else cout << n1;
        }
        // 输出多余的n2
        for (int i = 1; i <= b - numb; i ++ ) cout << n2;
    } else {
        // 如果k是偶数,说明是n1开头n1结束,不能把多余的n2放到尾部
        // 可以先不用输出最后一个n1
        for (int i = 1; i < k; i ++ ) {
            if (i & 1) cout << n2;
            else cout << n1;
        }
        // 在输出最后一个n1之前先把多余的n2输出
        for (int i = 1; i <= b - numb; i ++ ) cout << n2;
        // 再输出最后一个n1
        cout << n1;
    }
    cout << endl;
}

void solve() {
    int a, b;
    cin >> a >> b >> k;
    if (k == 0) {
        // k=0的特判
        if (a != 0 && b != 0) cout << -1 << endl;
        else {
            for (int i = 1; i <= a; i ++ ) cout << 0;
            for (int i = 1; i <= b; i ++ ) cout << 1;
            cout << endl;
        }
    } else {
        if (a == b && k > a + b - 1) {
            cout << -1 << endl;
            return ;
        }
        if (k > 2 * min(a, b)) cout << -1 << endl;
        else {
            // 如果0多,就先填0,否则先填1
            if (a >= b) sort_01(a, b, 0, 1);
            else sort_01(b, a, 1, 0);
        }
    }
}

int main() {
	int T = 1;
	cin >> T;
	while (T -- )
		solve();
	return 0;
} 

D 小红的01串(四)

有两种方法,我讲讲DP写法。

\(dp_i\) 表示走到 \(i\) 时的最小花费。容易看出 \(dp_1 = 0\) ,可以从第 \(i\) 个位置转移到最近相同的字符处或者最近不同的字符处。这个信息我们可以开一个数组预处理得到。令 \(suf_{i,0/1}\) 表示离 \(i\) 最近的 \(0/1\) 字符的位置, \(O(n)\) 预处理即可。

知道了以上信息后,我们得到以下状态转移式:

\(i\) 处为字符 \(0\) 时:

\(dp_{suf_{i, 0}} = \max(dp_{suf_{i,0}}, dp_i + x)\)

\(dp_{suf_{i,1}} = \max(dp_{suf_{i,1}}, dp_i + y)\)

\(i\) 处为字符 \(1\) 时:

\(dp_{suf_{i, 0}} = \max(dp_{suf_{i,0}}, dp_i + y)\)

\(dp_{suf_{i, 1}} = \max(dp_{suf_{i,1}}, dp_i + x)\)

递推求即可。

时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;

LL dp[N];
int suf[N][2];

void solve() {
	int n, x, y;
    cin >> n >> x >> y;
    string s; cin >> s;
    for (int i = 2; i <= n; i ++ ) dp[i] = 0x3f3f3f3f3f3f3f3f;
    int idx0 = -1, idx1 = -1;
    for (int i = n; i >= 1; i -- ) {
        suf[i][0] = idx0;
        suf[i][1] = idx1;
        if (s[i - 1] == '0') idx0 = i;
        else idx1 = i;
    }
    dp[1] = 0;
    for (int i = 1; i <= n; i ++ ) {
        char c = s[i - 1];
        if (c == '0') {
            if (suf[i][0] != -1) dp[suf[i][0]] = min(dp[suf[i][0]], dp[i] + x);
            if (suf[i][1] != -1) dp[suf[i][1]] = min(dp[suf[i][1]], dp[i] + y);
        } else {
            if (suf[i][1] != -1) dp[suf[i][1]] = min(dp[suf[i][1]], dp[i] + x);
            if (suf[i][0] != -1) dp[suf[i][0]] = min(dp[suf[i][0]], dp[i] + y);
        }
    }
    cout << dp[n] << endl;
}

int main() {
	int T = 1;
// 	cin >> T;
	while (T -- )
		solve();
	return 0;
} 

E 小红的01串(五)

一个比较套路的DP。我们可以用 \(dp_{i,j}\) 表示到第 \(i\) 个字符,模 \(13\) 的余数为 \(j\) 时的方案数。

可以遍历字符串,当遍历到第 \(i\) 个字符时,有以下几种情况:

  • \(s_i = 0\) 则说明当前位只能填 \(0\) ,枚举上一个可能转移过来的状态,即可以从 \(dp_{i-1,j}\) 转移过来。由于上一个状态模 \(13\) 的余数为 \(j\) ,因此添上 \(0\) 以后模 \(13\) 的余数变成 \(j*10 \% 13\) ,即 \(dp_{i,j*10\%13} += dp_{i-1,j}\)
  • \(s_i = 1\) 则说明当前位只能填 \(1\) ,枚举上一个可能转移过来的状态,即可以从 \(dp_{i-1,j}\) 转移过来。由于上一个状态模 \(13\) 的余数为 \(j\) ,因此添上 \(1\) 以后模 \(13\) 的余数变成 \((j*10+1) \% 13\) ,即 \(dp_{i,(j*10+1)\%13} += dp_{i-1,j}\)
  • \(s_i=?\) 则说明即可以填 \(0\) 也可以填 \(1\) ,把上面的转移写到一起即可。

时间复杂度 \(O(p\cdot n)\) 其中 \(p=13\)

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
const int MOD = 1e9 + 7;

LL dp[N][20];

void solve() {
	string s; cin >> s;
    int n = s.size();
    s = "+" + s;
    dp[0][0] = 1;
    for (int i = 1; i <= n; i ++ ) {
        if (s[i] != '?') {
            for (int j = 0; j < 13; j ++ ) {
                dp[i][(j * 10 + s[i] - '0') % 13] += dp[i - 1][j];
                dp[i][(j * 10 + s[i] - '0') % 13] %= MOD;
            }
        } else {
            for (int j = 0; j < 13; j ++ ) {
                dp[i][(j * 10 + 1) % 13] += dp[i - 1][j];
                dp[i][(j * 10 + 1) % 13] %= MOD;
                dp[i][(j * 10) % 13] += dp[i - 1][j];
                dp[i][(j * 10) % 13] %= MOD;
            }
        }
    }
//     for (int i = 1; i <= n; i ++ ) {
//         for (int j = 0; j < 13; j ++ ) cout << dp[i][j] << ' ';
//         cout << endl;
//     }
    cout << dp[n][0] << endl;
}

int main() {
	int T = 1;
// 	cin >> T;
	while (T -- )
		solve();
	return 0;
} 

\(bonus\):类似的思想大家还可以做做洛谷的 P3131

F 小红的01串(六)

碎碎念:第一眼看题意,这不是非常经典的线段树么。然后开始写,都把 push_down 写完了,结果发现 \(n\)\(1e9\) 的,要加个离散化才行。

建议正在学线段树的同学都可以做做这题,如果能独立把代码调出来的话相信你的收获一定会很大。

我们先思考 \(n \le 10^5\) 左右的情况。我们可以开一个线段树,维护以下信息:

  • 将当前区间修改为 \(0\) 开头的好串和 \(1\) 开头的好串的最小修改次数(分别记作 \(num0\)\(num1\)

首先考虑合并操作,我们在 push_up 的时候,当前节点的 \(num0\) 就是左子树的 \(num0\) 加上右子树的 \(num0\) 或者 \(num1\) ,取决于左子树维护的区间长度。如果是奇数,说明 \(0\) 开头的好串一定是 \(1\) 结尾,那么右子树需要从 \(0\) 开头,因此需要用 \(num0\) 更新,另外一个情况同理。

再考虑更新操作,注意到有两个区间修改操作,先考虑单种:

  • 若只有区间修改为 \(1\) 的话可以考虑加个 \(lazyTag1\) ,然后直接更新区间 \(num0\)\(num1\)
  • 若只有区间取反,可以考虑加个 \(lazyTag2\) ,然后交换区间 \(num0\)\(num1\) 即可。

接下来考虑复合操作:

  • 若对一个区间先取反后修改为 \(1\) ,相当于取反操作无效,只用管修改操作。
  • 若对一个区间先修改为 \(1\) 后再取反,相当于区间修改为 \(0\),直接更新 \(num0\)\(num1\)

因此我们看得出来,对于一个区间的两个 \(lazyTag\) ,他们的先后顺序是有区别的,因此我们可以将两个 \(lazyTag\) 记作是第几次操作,方便我们更新。

这里需要注意一点:若我们在 push_down 的过程中,发现离当前区间的操作的最近操作是取反,并且当前又更新了一个取反操作,那么我们应该要做的是把取反的 \(lazyTag\) 给取消掉,而不是更新取反的 \(lazyTag\) ,否则会影响下面子区间的取反操作。

好了,分析完以后,发现 \(n \le 10^9\) ,所以我们不能像刚刚一样每个叶节点只维护一个位置,因此要让每个叶节点维护一个区间,例如我们的操作区间依次是 \([1, 6],[2, 9],[6, 10]\) ,先变成左闭右开的区间,即 \([1,7),[2,10),[6,11)\) ,把端点记下来也就是 \(1, 2, 6, 7, 10, 11\) ,最后将相邻两个数之间合并成一个左闭右开的区间即可。\([1,2),[2,6),[6,7),[7,10),[10, 11)\) 。离散化后就可以转化了。

时间复杂度 \(O((q + n) \cdot \log n)\)

#include <bits/stdc++.h>
#define ls (u << 1)
#define rs (u << 1 | 1)
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 3e5 + 10;

struct qy {
    int opt, l, r;
} a[N];
struct node {
    int l, r;       // 管理的左右区间,用离散化之后的值代替
    int ranL, ranR; // 表示管理的左右区间
    int num0, num1; // 将当前区间修改为0开头的好串和1开头的好串的修改次数
    int lazy_tag;   // 更新区间翻转的lazy是第几次操作
    int lazy_1;     // 更新区间为1的lazy是第几次操作
} tr[N << 2];

vector<int> v;
int getIdx(int x) {
    return lower_bound(v.begin(), v.end(), x) - v.begin() + 1;
}

void calc(int u, int lt, int l1) {
    int len = tr[u].ranR - tr[u].ranL + 1;
    if (lt && l1 && lt > l1) {
        // 先1后翻,等价于赋0
        tr[u].num0 = len / 2;
        tr[u].num1 = (len + 1) / 2;
    } else if (l1) {
        // 剩下的情况就是翻后1或者有1无翻,直接赋值为1
        tr[u].num0 = (len + 1) / 2;
        tr[u].num1 = len / 2;
    } else if (lt) {
        swap(tr[u].num0, tr[u].num1);
    }
}

void push_down(int u) {
    if (tr[u].lazy_1) tr[ls].lazy_1 = tr[rs].lazy_1 = tr[u].lazy_1;
    if (tr[u].lazy_tag) {
        // down下去取反操作时,注意看最近的操作是不是取反
        // 如果是的话就需要更新子区间的lazy_tag为0
        if (tr[ls].lazy_1 < tr[ls].lazy_tag) tr[ls].lazy_tag = 0;
        else tr[ls].lazy_tag = tr[u].lazy_tag;
        if (tr[rs].lazy_1 < tr[rs].lazy_tag) tr[rs].lazy_tag = 0;
        else tr[rs].lazy_tag = tr[u].lazy_tag;
    }
    calc(ls, tr[u].lazy_tag, tr[u].lazy_1);
    calc(rs, tr[u].lazy_tag, tr[u].lazy_1);
    tr[u].lazy_1 = tr[u].lazy_tag = 0;
}

void push_up(int u) {
    tr[u].ranL = tr[ls].ranL, tr[u].ranR = tr[rs].ranR;
    // 更新num0
    tr[u].num0 = tr[ls].num0;
    int len_l = tr[ls].ranR - tr[ls].ranL + 1;
    if (len_l & 1) tr[u].num0 += tr[rs].num1;
    else tr[u].num0 += tr[rs].num0;
    // 更新num1
    tr[u].num1 = tr[ls].num1;
    if (len_l & 1) tr[u].num1 += tr[rs].num0;
    else tr[u].num1 += tr[rs].num1;
}

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r;
    if (l == r) {
        tr[u].ranL = v[l - 1];
        tr[u].ranR = v[l] - 1;
        int len = tr[u].ranR - tr[u].ranL + 1;
        tr[u].num0 = len / 2;
        tr[u].num1 = (len + 1) / 2;
        return ;
    }
    int mid = (l + r) >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    push_up(u);
}

void update(int u, int st, int ed, int opt, int num_opt) {
    // opt=1表示更新当前区间为1,否则就是翻转
    int l = tr[u].l, r = tr[u].r;
    if (st <= l && r <= ed) {
        if (opt == 1) {
            tr[u].lazy_1 = num_opt;
            calc(u, 0, num_opt);
        }
        if (opt == 2) {
            if (tr[u].lazy_1 < tr[u].lazy_tag) tr[u].lazy_tag = 0;
            else tr[u].lazy_tag = num_opt;
            calc(u, num_opt, 0);
        }
        return ;
    }
    push_down(u);
    int mid = (l + r) / 2;
    if (st <= mid) update(ls, st, ed, opt, num_opt);
    if (ed > mid) update(rs, st, ed, opt, num_opt);
    push_up(u);
}

node query(int u, int st, int ed) {
    int l = tr[u].l, r = tr[u].r;
    if (st <= l && r <= ed) return tr[u];
    push_down(u);
    int mid = (l + r) >> 1;
    node ans;
    ans.num0 = -1;
    if (st <= mid) {
        node tmp = query(ls, st, ed);
        ans = tmp;
    }
    if (ed > mid) {
        node tmp = query(rs, st, ed);
        if (ans.num0 == -1) ans = tmp;
        else {
            // 更新答案的num0
            int lenl = ans.ranR - ans.ranL + 1;
            if (lenl & 1) ans.num0 += tmp.num1;
            else ans.num0 += tmp.num0;
            // 更新答案的num1
            if (lenl & 1) ans.num1 += tmp.num0;
            else ans.num1 += tmp.num1;
            ans.ranR = tmp.ranR;
        }
    }
    return ans;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("1.in", "r", stdin);
    freopen("1.out", "w", stdout);
#endif
    int m, q;
    cin >> m >> q;
    for (int i = 1; i <= q; i ++ ) {
        cin >> a[i].opt >> a[i].l >> a[i].r;
        v.push_back(a[i].l);
        v.push_back(a[i].r + 1);
    }
    sort(v.begin(), v.end());
    v.erase(unique(v.begin(), v.end()), v.end());
    int n = v.size() - 1;
    build(1, 1, n);
    for (int i = 1; i <= q; i ++ ) {
        int opt = a[i].opt;
        int l = getIdx(a[i].l) , r = getIdx(a[i].r + 1) - 1;
        if (opt == 1) update(1, l, r, 1, i);
        else if (opt == 2) update(1, l, r, 2, i);
        else {
            node ans = query(1, l, r);
            cout << min(ans.num0, ans.num1) << endl;
        }
    }
    return 0;
}
posted @ 2024-12-17 10:16  Time_Limit_Exceeded  阅读(5)  评论(0编辑  收藏  举报