牛客周赛73题解

A.小红的正整数构造

小红拿到了一个区间,她希望你在这个区间内找到一个正整数,满足该正整数是 的倍数。你能帮帮她吗?

数据很小,可以直接枚举

将左端点和右端点分别除以x,得到不大于的倍数,若这两个是相等的,则说明没有的倍数

特判的情况,输出比大的第一个的倍数即可

// C++

void solve(){
    int l , r , x;
    cin >> l >> r >> x;
    if(l == r && l % x == 0){
        cout << l << endl;
        return;
    }
    int L = l / x , R = r / x;
    if(L == R){
        cout << -1 << endl;
        return;
    }
    else cout << x * L + x << endl;
}

B.小红的区间构造

小红拿到了正整数 ,她希望你找到一个长度为 的区间,满足区间内恰好有 个数是 的倍数。你能帮帮她吗?

假设某一个的倍数为,讨论三种情况:

  1. 区间刚好略过
  2. 区间刚好包含
  3. 区间的左端点就是

可以证明,只要这三种情况全不满足条件,那就找不到满足条件的区间

void solve(){
    int n , k , x;
    cin >> n >> k >> x;
    // 枚举3种情况
    for (int l = x - 1; l <= x + 1; ++l) {
        int r = l + k - 1;
        int count = r / x - (l - 1) / x;
        if (count == n) {
            cout << l << " " << r << endl;
            return;
        }
    }
    cout << -1 << endl;
}

C.小红的排列构造

小红定义一个仅由 两个字符构成的 和一个排列 是匹配的,当且仅当满足以下条件:
,则排列的前 项元素恰好也构成一个排列;
,则排列的前 项元素无法构成一个排列;
现在小红拿到了一个长度为 串。请你构造一个排列使得它们是匹配的。
长度为 的排列是由 个整数、按任意顺序组成的数组,其中每个整数恰好出现一次。例如, 是一个长度为 的排列,而 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

因为构造出来的数组本身也是一个排列,所以先判断01串最后一位,如果是0,则直接返回-1

由于对于,若当前位是1,则数组前位是长度为的排列,包含了的所有整数,而若当前位是0,则前位不包含的所有整数。那么我们不难想到的一种方案是:

对于,将的整数中的某一个整数藏起来,再遇到再输出这一个整数

alt

于是我们可以把整个01串分为若干个串,我们设他的左端点为,右端点为,的区间构成一个的排列,并且从开始的每一个,其在数组的对应位置上都应当缺少一个,而缺少的在1处补上即可

// C++
void solve(){
    int n;
    cin >> n;
    string t;
    cin >> t;
    vector<int> arr;// 存放每一个0000……1串的左端点
    arr.pb(1);// 第一个放入1
    vector<int> a(n , -1);// 存放数组
    vector<int> vis(n + 1 , 0);// 标记左端点已访问
    vector<int> re;// 把剩余的数依次放入数组
    for(int i = 0;i < n;i ++){
        // 当前位置为1,补上左端点对应的值
        if(t[i] == '1'){
            a[i] = arr.back();
            vis[a[i]] = 1;// 标记左端点已访问
            arr.pb(i + 2);// 放入下一个左端点
        }
    }
    // 特判末尾不为0
    if(t[n - 1] == '0'){
        cout << -1 << endl;
        return;
    }
    // 记录剩余的数
    for(int i = 1;i <= n;i ++){
        if(vis[i] == 0) re.pb(i);
    }
    int p = 0;
    for(int i = 0;i < n;i ++){
        if(a[i] == -1) a[i] = re[p] , p ++;// 放入数组中
    }
    for(int i = 0;i < n;i ++){
        cout << a[i] << " \n"[i == n - 1];// 输出数组
    }
}

D.小红的01子序列构造(easy)

小红拿到了一个仅由 两个字符构成的 ,她希望你找到一个区间,满足该区间内有恰好 子序列。
子序列为从原字符串中删除任意个(可以为零、可以为全部)字符得到的新字符串。

区间问题,考虑使用滑动窗口

思考窗口两端对区间的贡献

alt

观察上图我们发现:

在右端点右移的过程中,如果新加入的字符为"0",由于01子序列必须由0开始,所以对区间01子序列数量没有影响

而新加入的字符"1"可以和区间内已有的所有"0"匹配成一对01子序列

即右端的0的贡献为0,右端的1的贡献为表示区间内0的数量

在左端点右移的过程中,如果移除的字符为"1",由于01子序列必须由1结束,所以对区间01子序列数量没有影响

而移除的字符"0"会和区间内已有的所有"1"失去配对

即左端的1的贡献为0,右端的0的贡献为表示区间内1的数量

// C++
void solve(){
    int n , k;
    cin >> n >> k;
    string s;
    cin >> s;
    int l = 0 , r = 0;
    int cnt = 0 , cnt0 = 0 , cnt1 = 0;
    while(r < n){
        // 移动右端点
        while(r < n && cnt < k){
            cnt1 += (s[r] == '1');
            cnt0 += (s[r] == '0');// 利用布尔表达式更新cnt1和cnt0,表达式成立+1,不成立+0
            cnt += (s[r] == '1')?cnt0:0;// 计算右端点贡献
            r ++;
        }
        // 区间01子序列个数为k,直接输出答案
        if(cnt == k){
            cout << l << " " << r << endl;
            return;
        }
        // 移动左端点
        while(cnt > k){
            cnt1 -= (s[l] == '1');
            cnt0 -= (s[l] == '0');// 利用布尔表达式更新cnt1和cnt0,表达式成立-1,不成立-0
            cnt -= (s[l] == '0')?cnt1:0;// 计算左端点贡献
            l ++;
        }
    }
    // 没找到,输出-1
    cout << -1 << endl;
}

E.小红的二分图构造

小红希望你构造一个二分图,满足第 个节点的度数恰好等于 。你能帮帮她吗?
二分图是一张满足如下条件的图:它的节点可以被分成两个不相交的集合 ,使得图中的每一条边都连接 中的一个节点与 中的一个节点。

由于图中的每一条边都连接中的一个节点与中的一个节点,设一条边连接了点和点,的搭建会同时占用的度数和的度数

这意味着集合中的度数总和与集合中的度数总和必须是相等的

因为如果,我们假设,我们搭建边把中所有点的度数占满后,中仍会存在无法连接的边

所以我们只要将所有的点分成两个总度数相等的集合,然后暴力连边即可

// C++
void solve(){
    int n;
    cin >> n;
    vector<int> a(n + 1);
    int sum = 0;
    for(int i = 1;i <= n;i ++){
        cin >> a[i];
        sum += a[i];
    }
    // 总度数是奇数,直接输出-1
    if(sum & 1){
        cout << -1 << endl;
        return;
    }
    sum = sum >> 1;// 取一半作为U和V的总度数
    // 01背包
    vector<vector<int>> dp(n + 1 , vector<int>(sum + 1 , 0));// 构建dp表,dp[i][j]表示前i个元素是否可以凑出j的总和
    dp[0][0] = 1;// 0个元素可以凑出0的总和
    for(int i = 1;i <= n;i ++){
        dp[i] = dp[i - 1];// 继承前一个的状态
        for(int j = sum;j >= a[i];j --){
            dp[i][j] |= dp[i  - 1][j - a[i]];// 状态转移,如果不选第i个元素可以凑出j-a[i],则选了就可以凑出j
        }
    }
    // 无法分成两半,直接输出-1
    if(!dp[n][sum]){
        cout << -1 << endl;
        return;
    }
    // 回溯01背包,找到具体方案
    int val = sum;// 暂存sum
    vector<int> u , v;
    for(int i = n;i >= 1;i --){
        // 选不选没影响的,放入U
        if(dp[i][val] == dp[i - 1][val]){
            u.emplace_back(i);
        }
        // 有影响(说明在01背包的过程中选中了)放入v
        else{
            v.emplace_back(i);
            val -= a[i];// 更新剩余需要的度数
        }
    }
    vector<pair<int , int>> ans;// 存储答案
    // 遍历U,使其所有节点连满
    for(int U : u){
        while(a[U]){
            // 遍历v
            for(int V : v){
                // 两个点都没连满,将他们连起来
                if(a[U] && a[V]){
                    a[U] --;
                    a[V] --;
                    ans.emplace_back(U , V);
                }
            }
        }
    }
    cout << ans.size() << endl;
    for(int i = 0;i < ans.size();i ++) {
        cout << ans[i].first << " " << ans[i].second << endl;
    } 
}

F.小红的01子序列构造(hard)

拼尽全力战胜他!!!!!

小红希望你构造一个长度为 串,满足 个性质:对于 ,区间 有恰好 子序列。
给定的限制区间为两两包含关系。即对于 ,若 ,则 ,反之亦然。
子序列为从原字符串中删除任意个(可以为零、可以为全部)字符得到的新字符串。

题目中说到,给定的限制区间为两两包含关系,即大区间必定完全包含小区间,那么我们应从小到达构造字符串,让大区间在小区间的基础上构造

先解决如何在小区间内给定x , y , k构造字符串

假设,我们观察两种不同的构造方式:

,此时

,此时

很显然上面两种情况分别是k可能的最大值和可能的最小值,即

由于每个0的后面都可能有~个1,所以可以表示为:

至此,我们证明了,对于都必定可以构造出符合条件的字符串

接下来我们思考对于任意的k,如何构造符合条件的字符串

alt 可以采用动态规划的方式,假设对于一个区间上的一个点,假设之前的字符串都已经构造完成了,剩下还需要填入个1,配出个01子序列 假设这一位填0,则其可以和后面所有的个1构成01子序列,即这一位的0的贡献为

填上0之后,需要配出的01子序列的数量变成了,我们当然不希望这个值变成负数,因此,当这一位的0的贡献超过了时,我们改为在这一位填入1即可 最终在小区间构造01串的算法写成函数变为:

vector<int> a(N);// 创建数组用于存放01
// 需要判断这个区间是否可以构造出符合条件的字符串,所以函数返回类型为布尔型
bool build(int l , int r , int x , int y , int k) {
    if(max(x , max(y , k)) == 0) return true;// 区间长度为0,不需要构造,直接返回true即可
    if(k > x * y) return false;// 无法构造,返回false
    for(int i = l;i <= r;i ++) {
        // 填0的贡献不超过k,填0并更新x和k
        if(k >= y) {
            s[i] = 0;
            x --;
            k -= y;
        }
        // 填1并更新y
        else {
            s[i] = 1;
            y --;
        }
    }
    // 返回true表示构造成功
    return true;
}

实现了对小区间的构造后,我们解决小区间确定了的情况下,如何构造大区间的问题

由于所有区间都是两两包含关系,所以小区间会将大区间分为3个部分:

alt

我们设已经构造好的区间为区间,左边的区间为区间,右边的区间为区间

可以得到区间的长度为

区间的长度为

已知已经构造好的区间里有,当前大区间里有

区间有区间有

有:

固定后,其余的所有量都固定了

那么我们可以枚举,判断是否可以构造即可

对于

我们可以算出无论区间和区间如何排列,该区间的贡献至少为:

因此我们只需要让

由于对于一个小区间,可能的分布在之间,所以越小越有可能构造出符合要求的01串

那么我们只需要对每一个,让尽可能大,然后尝试构造即可

// C++
vector<int> a;// 创建数组用于存放01
vector<int> l , r;// 创建数组用于存放每一次询问的左右边界
// 需要判断这个区间是否可以构造出符合条件的字符串,所以函数返回类型为布尔型
bool build(int l , int r , int x , int y , int k) {
    if(x == 0 && y == 0 && k == 0) return true;// 区间长度为0,不需要构造,直接返回true即可
    if(k > x * y || r - l + 1 != x + y) return false;// 无法构造,返回false
    for(int i = l;i <= r;i ++) {
        // 填0的贡献不超过k,填0并更新x和k
        if(k >= y) {
            a[i] = 0;
            x --;
            k -= y;
        }
        // 填1并更新y
        else {
            a[i] = 1;
            y --;
        }
    }
    // 返回true表示构造成功
    return true;
}
// 构造cmp规则,按区间大小升序
bool cmp(int i , int j){
    return r[i] - l[i] < r[j] - l[j];
}
void solve(){
    int n , m;
    cin >> n >> m;
    a.resize(n + 1);
    l.resize(m);
    r.resize(m);
    vector<int> x(m) , y(m) , k(m) , p(m);
    for(int i = 0;i < m;i ++){
        cin >> l[i] >> r[i] >> x[i] >> y[i] >> k[i];
        p[i] = i;
    }
    // 根据区间的大小对每次询问排序
    sort(p.begin() , p.end() , cmp);
    for(int j = 0;j < m;j ++){
        int i = p[j];
        // 如果是最小的区间,直接构造
        if(j == 0){
            if(!build(l[i] , r[i] , x[i] , y[i] , k[i])){
                cout << -1 << endl;
                return;
            }
        }
        else{
            int last = p[j - 1];
            int lastl = l[last];
            int lastr = r[last];
            int lastx = x[last];
            int lasty = y[last];
            int lastk = k[last];// 获取上一次构造的信息
            int Ll = l[i] , Lr = lastl - 1 , Ld = Lr - Ll + 1;
            int Rl = lastr + 1 , Rr = r[i] , Rd = Rr - Rl + 1;// 获取L区间和R区间的范围和大小
            int flag = 0;
            // 枚举每一种可能的Lx
            for(int Lx = max(0 , x[i] - lastx - Rd);Lx <= min(Ld , x[i] - lastx);Lx ++){
                int Rx = x[i] - lastx - Lx;
                int Ly = Ld - Lx;
                int Ry = Rd - Rx;// 计算对应的左右区间的xy
                int K = Lx * (lasty + Ry) + Ry * lastx + lastk;// 计算区间最小的贡献
                int dk = k[i] - K;// 计算需要的贡献
                int Lk = min(Lx * Ly , dk) , Rk = dk - Lk;
                if(dk < 0) continue;// 无法构造,继续枚举
                if(build(Ll , Lr , Lx , Ly , Lk) && build(Rl , Rr , Rx , Ry , Rk)){
                    flag = 1;
                    break;
                }
            }
            if(!flag){
                cout << -1 << endl;
                return;
            }
        }
    }
    for(int i = 1;i <= n;i ++) cout << a[i];
    cout << endl;
}

总结

最近的周赛确实越来越难了,这次的题对思维能力有很大的要求

A题唯一的签到题,不用多说

B题是一个非常烦的题,如果没有想到合适的方法可能会卡很久很久(初见端倪)

C题可能比B题还要简单(),是众多名字里带构造的题里唯一一个正常的构造题,用正常的构造题思路去写没有太大的难度

D题考验我们对滑动窗口的掌握和对端点贡献的判断,本质上是一道简单的题

E题要求我们对图论的基本知识足够了解,只要基本功扎实就没什么问题

F题重量级,同时考察了:构造、动态规划、动态规划和动态规划,非常难非常好,sukisuki🥰🥰🥰