题解 | #选数Ⅱ#

选数Ⅱ

https://www.nowcoder.com/practice/3647d44e120b4ec19536a8236d251a06

题目链接

选数Ⅱ

题目描述

给定一个长度为 、只包含 '0' 和 '1' 的字符串 。有 次独立的询问,每次询问给出一个区间 。对于每个询问,我们需要在子串 中选取若干字符进行染色,要求:

  1. 只有字符 '1' 能被染色。
  2. 不能有两个相邻的字符同时被染色。

目标是计算在满足以上条件下,最多能染色的字符数量。

解题思路

本题要求在字符串的子区间中,选取满足条件的字符进行染色,以使数量最多。在仔细分析题目和标准解法后,我们发现题目的核心规则可以分解为两个独立的部分:

  1. 字符 '0' 的染色:题目中的不相邻约束 “不存在任意两个相邻的字符 1 同时被染色” 只针对字符 '1'。这意味着字符 '0' 没有限制,可以全部被染色。
  2. 字符 '1' 的染色:需要遵循不相邻的规则。

因此,最终的答案等于 (区间内 '0' 的数量) + (区间内不相邻 '1' 的最大数量)

我们可以分别计算这两个部分:

1. 计算区间内 '0' 的数量

这可以通过一个简单的前缀和数组 sum_zeros 来实现。sum_zeros[i] 存储原字符串前 个字符中 '0' 的数量。对于任意查询 ,'0' 的数量就是 sum_zeros[r] - sum_zeros[l-1]

2. 计算区间内不相邻 '1' 的最大数量

这是一个动态规划问题,但可以用贪心二进制倍增(Binary Lifting)来高效求解。

  • 贪心策略:为了选取尽可能多的 '1',最优的策略是从区间的右端点 出发,贪心地选择最靠右的那个可以被选择的 '1',然后从这个位置向前寻找下一个可以被合法选择的最靠右的 '1',如此往复,直到跳出区间

  • 构建“跳转”关系:我们可以预处理一个数组 parent[i],它表示“如果我当前选择了位置 (假设 ),那么在我之前,下一个可以合法选择的位置在哪里?”。

    • 如果 ,那么前一个位置没有限制,我们可以从上一个 '1' 的位置跳转而来。
    • 如果 ,那么我们不能选择 ,必须从 之前的那个 '1' 的位置跳转而来。
  • 二进制倍增优化:一步步地往前跳转效率太低。我们可以构建一个二维数组 f[i][j],表示从位置 开始,连续向前跳转 次到达的位置。这可以使用动态规划在 的时间内预处理出来。

  • 查询:对于查询 ,我们从 开始,利用 f 数组进行倍增跳转,每次都尝试跳跃最大的步数,同时保证不会跳出区间下界 。通过累加跳跃的步数,我们就能在 的时间内计算出从 最多能选择多少个不相邻的 '1'。

将两部分结果相加,即得到最终答案。总时间复杂度为

代码

#include <iostream>
#include <vector>
#include <string>
#include <cmath>

using namespace std;

int main() {
    // 开启快速IO
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;
    string s_str;
    cin >> s_str;
    s_str = " " + s_str; // 字符串下标从1开始,方便处理

    // sum_zeros[i] 记录前i个字符中'0'的数量
    vector<int> sum_zeros(n + 1, 0);
    // f[i][j] 记录从位置i向前跳2^j步到达的位置
    vector<vector<int>> f(n + 2, vector<int>(20, 0));
    
    // last1: 最近的'1'的位置, prev_last1: 次近的'1'的位置
    int last1 = 0, prev_last1 = 0; 

    for (int i = 1; i <= n; i++) {
        sum_zeros[i] = sum_zeros[i - 1];
        if (s_str[i] == '1') {
            // 如果当前'1'与上一个'1'相邻, 则父节点是上上个'1'
            if (last1 > 0 && last1 + 1 == i) {
                f[i][0] = prev_last1;
            } else { // 否则父节点是上一个'1'
                f[i][0] = last1;
            }
            prev_last1 = last1;
            last1 = i;
        } else {
            sum_zeros[i]++;
            f[i][0] = last1; // '0' 的父节点是上一个'1'
        }
        // 使用DP构建倍增表
        for (int j = 1; j < 20; j++) {
            f[i][j] = f[f[i][j-1]][j-1];
        }
    }

    while (q--) {
        int l, r;
        cin >> l >> r;

        // 1. 计算区间内'0'的数量
        int zeros_count = sum_zeros[r] - sum_zeros[l - 1];

        // 2. 倍增计算不相邻'1'的最大数量
        int ones_count = 0;
        int current_pos = r;
        
        // 从r开始贪心选择'1'
        if (s_str[current_pos] == '1') {
             ones_count = 1;
        } else {
            // 如果r是'0', 则从它之前最近的'1'开始
            current_pos = f[r][0];
            if(current_pos >= l) {
                ones_count = 1;
            }
        }
        
        // 如果起点在区间内,开始倍增跳转
        if (current_pos >= l) {
            for (int i = 19; i >= 0; i--) {
                // 尝试跳2^i步,如果目的地仍在区间内,则执行跳转
                if (f[current_pos][i] >= l) {
                    ones_count += (1 << i);
                    current_pos = f[current_pos][i];
                }
            }
        } else {
            ones_count = 0; // 起点已在区间外
        }

        cout << zeros_count + ones_count << "\n";
    }

    return 0;
}
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int q = sc.nextInt();
        String s = " " + sc.next(); // 字符串下标从1开始

        // sumZeros[i] 记录前i个字符中'0'的数量
        int[] sumZeros = new int[n + 1];
        // f[i][j] 记录从位置i向前跳2^j步到达的位置
        int[][] f = new int[n + 2][20];
        
        // last1: 最近的'1'的位置, prevLast1: 次近的'1'的位置
        int last1 = 0, prevLast1 = 0;

        for (int i = 1; i <= n; i++) {
            sumZeros[i] = sumZeros[i - 1];
            if (s.charAt(i) == '1') {
                // 如果当前'1'与上一个'1'相邻, 则父节点是上上个'1'
                if (last1 > 0 && last1 + 1 == i) {
                    f[i][0] = prevLast1;
                } else { // 否则父节点是上一个'1'
                    f[i][0] = last1;
                }
                prevLast1 = last1;
                last1 = i;
            } else {
                sumZeros[i]++;
                f[i][0] = last1; // '0' 的父节点是上一个'1'
            }
            // 使用DP构建倍增表
            for (int j = 1; j < 20; j++) {
                f[i][j] = f[f[i][j - 1]][j - 1];
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int k = 0; k < q; k++) {
            int l = sc.nextInt();
            int r = sc.nextInt();

            // 1. 计算区间内'0'的数量
            int zerosCount = sumZeros[r] - sumZeros[l - 1];
            
            int onesCount = 0;
            int currentPos = r;

            // 从r开始贪心选择'1'
            if (s.charAt(currentPos) == '1') {
                onesCount = 1;
            } else {
                 // 如果r是'0', 则从它之前最近的'1'开始
                currentPos = f[r][0];
                if (currentPos >= l) {
                    onesCount = 1;
                }
            }

            // 如果起点在区间内,开始倍增跳转
            if (currentPos >= l) {
                for (int i = 19; i >= 0; i--) {
                    // 尝试跳2^i步,如果目的地仍在区间内,则执行跳转
                    if (f[currentPos][i] >= l) {
                        onesCount += (1 << i);
                        currentPos = f[currentPos][i];
                    }
                }
            } else {
                 onesCount = 0; // 起点已在区间外
            }

            sb.append(zerosCount + onesCount).append("\n");
        }
        System.out.print(sb.toString());
    }
}
import sys

def main():
    n, q = map(int, input().split())
    s = " " + input() # 字符串下标从1开始

    # sum_zeros[i] 记录前i个字符中'0'的数量
    sum_zeros = [0] * (n + 1)
    # f[i][j] 记录从位置i向前跳2^j步到达的位置
    f = [[0] * 20 for _ in range(n + 2)]
    
    # last1: 最近的'1'的位置, prev_last1: 次近的'1'的位置
    last1, prev_last1 = 0, 0

    for i in range(1, n + 1):
        sum_zeros[i] = sum_zeros[i - 1]
        if s[i] == '1':
            # 如果当前'1'与上一个'1'相邻, 则父节点是上上个'1'
            if last1 > 0 and last1 + 1 == i:
                f[i][0] = prev_last1
            else: # 否则父节点是上一个'1'
                f[i][0] = last1
            prev_last1 = last1
            last1 = i
        else:
            sum_zeros[i] += 1
            f[i][0] = last1 # '0' 的父节点是上一个'1'
        
        # 使用DP构建倍增表
        for j in range(1, 20):
            f[i][j] = f[f[i][j - 1]][j - 1]

    for _ in range(q):
        l, r = map(int, input().split())

        # 1. 计算区间内'0'的数量
        zeros_count = sum_zeros[r] - sum_zeros[l - 1]
        
        ones_count = 0
        current_pos = r

        # 从r开始贪心选择'1'
        if s[current_pos] == '1':
            ones_count = 1
        else:
            # 如果r是'0', 则从它之前最近的'1'开始
            current_pos = f[r][0]
            if current_pos >= l:
                ones_count = 1

        # 如果起点在区间内,开始倍增跳转
        if current_pos >= l:
            for i in range(19, -1, -1):
                # 尝试跳2^i步,如果目的地仍在区间内,则执行跳转
                if f[current_pos][i] >= l:
                    ones_count += (1 << i)
                    current_pos = f[current_pos][i]
        else:
            ones_count = 0 # 起点已在区间外
            
        print(zeros_count + ones_count)

if __name__ == "__main__":
    main()

算法及复杂度

  • 算法:前缀和、贪心、二进制倍增
  • 时间复杂度:预处理前缀和数组 sum_zeros 需要 。预处理二进制倍增数组 f 需要 。每次查询都进行一次倍增跳转,需要 。总共有 次查询,因此总时间复杂度为
  • 空间复杂度sum_zeros 数组需要 的空间,f 数组需要 的空间。因此,总空间复杂度为
全部评论

相关推荐

不愿透露姓名的神秘牛友
08-28 11:55
点赞 评论 收藏
分享
程序员牛肉:1.大头肯定是院校问题,这个没啥说的。 2.虽然有实习,但是实习的内容太水了,在公司待了七个月的时间,看起来就只做了jwt和接入redis。爬取新闻,数据导入。这几个需求值得你做七个月吗?这不就是三四个月的工作量吗?我要是面试官的话真心会认为你能力不太行。所以既然有实习了,一定要好好写,像是Swagger这种东西是真没必要写上去,就拉一个包的事情。 3.我个人觉得话,在校生不要把自己当社招看,除非你的项目是特别牛逼,特别有名的含金量,否则不要写这种密密麻麻的一串子工作职责。你的项目只有一个作用,就是供面试官从中来抽取八股对你进行拷打。 但是你现在这个看不来什么技术点,可以改一下,详细表述一下你用什么技术实现了什么功能,在实现这个功能的过程中,你解决了什么难题。
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务