首页 > 试题广场 >

病毒检测

[编程题]病毒检测
  • 热度指数:3068 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32M,其他语言64M
  • 算法知识视频讲解
小明最近在做病毒自动检测,他发现,在某些library 的代码段的二进制表示中,如果包含子串并且恰好有k个1,就有可能有潜在的病毒。library的二进制表示可能很大,并且子串可能很多,人工分析不可能,于是他想写个程序来先算算到底有多少个子串满足条件。如果子串内容相同,但是开始或者结束位置不一样,则被认为是不同的子串。
注:子串一定是连续的。例如"010"有6个子串,分别是 "0, "1", "0", "01", "10", "010"


输入描述:
第一行是一个整数k,表示子串中有k个1就有可能是病毒。其中 0 <= k <= 1 000 000

第二行是一个字符串,就是library的代码部分的二进制表示。字符串长度 <= 1 000 000。并且字符串中只包含"0"或"1".


输出描述:
输出一个整数,所有满足只包含k个1的子串的个数。
示例1

输入

1
1010

输出

6

说明

满足条件的子串有:"1", "1", "10", "01", "10", "010".
示例2

输入

2
01010

输出

4

说明

满足条件的子串有: "101", "0101", "1010", "01010".
示例3

输入

100
01010

输出

0
动态规划思路,当前状态依赖前k个状态并且需维护当前状态为后面服务,需要k+1空间,但是需要开辟k+2个空间,其中一个空间来消除掉之前状态的值。
import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner scanner=new Scanner(System.in);
        int k=scanner.nextInt();
        scanner.nextLine();
        String s=scanner.nextLine();
        int num=0,len=s.length();
        int[] dp=new int[k+2];
        long result=0;
        dp[0]=1;
        for(char c:s.toCharArray()){
            if(c=='1') num++;
            if(num-k>=0) result+=dp[(num-k)%(k+2)];
            dp[(num+1)%(k+2)]=0;
            dp[num%(k+2)]++;
        }
        System.out.println(result);
    }
}

发表于 2020-03-16 02:37:23 回复(8)
首先来回顾一下题意:
这是一个子串满足问题,输入一个由01字符组成的字符串str,求满足包含k1的连续字串个数。
毫不夸张的说,可能大部分人第一眼就是求出所有子串,再计数所有满足k的子串,但仔细一想,问题没有那么简单!

思路就是,采用滑动窗口的方式,从字符串数组左侧依次向右滑动,直至满足k个1,在下一个数不为1且数组不越界的情况下窗口向右滑动,同时计数。
核心代码如下:
public class Main {
    public static int NumSubString(int k, String str) {
        char[] chars = str.toCharArray();
        int res = 0;
        int len = chars.length;
        for (int i = 0; i < len; i++) {
            if (chars[i] == '1') {
                res++;
            }
        }
        if (res < k) {
            return 0;
        }
        res = 0;
        for (int i = 0; i < len; i++) {
            /*滑动索引*/
            int index = i;
            int count = 0;
            while (count < k && index < len) {
                if (chars[index] == '1' && ++count == k) {
                    res++;
                    index++;
                    break;
                }
                index++;
            }
            /*满足k后继续右滑*/
            while (index < len && chars[index] != '1') {
                res++;
                index++;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int k = sc.nextInt();
        sc.nextLine();
        String str = sc.next();
        System.out.println(NumSubString(k,str));
    }

编辑于 2020-04-11 23:12:07 回复(6)
package 快手2020校园招聘秋招笔试工程C试卷;
import java.lang.*;
import java.util.*;

/**
 * Project: 通过率:100%
 * Author : en_zhao
 * Email  : 
 * Date   : 2020/08/03
 */

public class Main001{
    public static void main(String[] args){

        Scanner sc = new Scanner(System.in);
        int k = sc.nextInt();
        sc.nextLine();
        String str = sc.nextLine();
        //防溢出
        long res = 0;
        long[] sum = new long[str.length() + 1];
        char[] str_arr = str.toCharArray();
        int count = 0;
        //时间:O(n)扫描  空间: O(n)
        //sum[i]表示第(i+1)个1左边的0的个数
        //最后 记录末尾0的个数 数据可能造成空间浪费
        for(int i = 0;i < str.length();i++){
            if(str_arr[i] == '1'){
                count++;
            }else{ //记录第n个1左边0的个数
                sum[count]++;
            }
        }

                //计算res
        if(k == 0){ // 当k==0 直接求连续0的子串之和
            for(int i = 0; i <= count;i++){
                res += sum[i]*(sum[i]+1)/2;
            }
        }else{ // 当k!=0 求左右两边的子串组合(+1是因为空串"")
            for(int i = 0; i + k <= count;i++){
                res += (sum[i]+1) * (sum[i+k] + 1);
            }
        }

        System.out.println(res);

    }
}

编辑于 2020-08-03 19:15:33 回复(0)
思考了三个小时,终于完全将1楼大佬的思路理解透彻,并自己做了些修改
ps : 没有注释看起来是真的费劲,初期无从下手,全靠猜
import java.util.Scanner;
public class Main{
	
	/**
	 * 仔细观察和计算,相信大家都能够发现,对于任何一个已经计算好的字符串,往它的后面追加一个字符后
	 * 对结果的有影响的字符串只是倒数第k+1个'1'之后的子串
	 * 例: k = 2; str = 01010; result = 4
	 * 如果往str后面追加1,str = 010101,对最终结果有影响的是0101(01'0101')子串
	 * 如果往str后面追加0,str = 010100,对最终结果有影响的是010100('010100')子串
	 * 影响主要有两个方面:
	 *   影响一:不管追加的是0,还是1,最终结果都会增加1个匹配字串
	 *   影响二:如果倒数第k位1前面有0,则每个0都会增加1个匹配子串
	 * 
	 * 那么可以根据这一特性保存最后k位'1'左边的0的数量,就能够利用前面字串已经计算好的结果,来对追加后字符串的结果进行计算
	 * 
	 * 
	 */
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		int k = Integer.parseInt(input.nextLine());
		String str = input.nextLine();
		long result = 0;  //结果可能会非常大
		int oneNum = 0;
        int[] kCharNum = new int[k + 1]; //建立k+1位的数组,记录最后k位'1'左边0的数量,以及后面可能出现的'1'左边0的数量,所以需要k+1位
        kCharNum[0] = 1;  //因为影响一,所以提前加1,也是为了解决k=0的情况
        for(int i = 0; i < str.length(); i++){
        	if(str.charAt(i) == '1'){
        		oneNum++;
        		//num已经加1了,需要将倒数k+1位'1'的数据清空,重新利用计数
        		kCharNum[oneNum % (k + 1)] = 0;
        	}
        	if(oneNum >= k){
        		//result += 从当前最近1的位子往左数k位的那个1,它左边0的数量  + 1
        		//因为已经提前加了1,所以只需加上倒数第k位0的数量(里面已经包含了加1)
    			result += kCharNum[(oneNum - k) % (k + 1)];
    		}
        	//如果当前字符是'0',那么加1
        	//如果当前字符是'1',则提前加1
        	//所以都需要加1
        	kCharNum[oneNum % (k + 1)]++;
        }
		System.out.println(result);
		
		input.close();
		
	}
	
	
}


发表于 2020-05-01 16:00:03 回复(0)

上个 的暴力吧:

#include <bits/stdc++.h>
const int maxn = 1e6 + 5;
char s[maxn];
int sum[maxn],k;
int main() {
    scanf("%d%s", &k,s + 1);
    int n = strlen(s + 1);
    for (int i = 1; i <= n; ++i)    sum[i] = sum[i - 1] + (s[i] == '1');
    long long ans = 0;
    for (int j = 1; j <= n; ++j) {
        ans += std::upper_bound(sum, sum + j, sum[j] - k) -
                std::lower_bound(sum, sum + j, sum[j] - k);
    }
    printf("%lld\n", ans);
    return 0;
}
发表于 2020-03-20 13:19:03 回复(0)
这题考什么?
发表于 2020-06-17 15:38:55 回复(2)

这个明明是个O(n)的尺取,额外空间是O(1)的。


弄个左右指针框住一段恰好k个1的最短的区间,然后左右延伸出去数一数两边的0。

发表于 2020-03-21 07:31:10 回复(5)
思路: 使用两个辅助vector,一个记录1的位置v1,一个记录某个1的前面0的数量v2,用一个长度为k的窗口在v1上滑动,满足条件的子串的数量与窗口首尾位置对应的v2记录的0的数量相关,将每一个窗口的结果相加即可。结果应该设置成long long类型,有一个测试用例有很多0。
#include <iostream>
(720)#include <vector>
#include <string>

using namespace std;

int main(){
    int k, zero_cnt = 0;
    long long res = 0;
    string s;
    char ch;
    cin >> k;
    cin >> s;
    vector<int> keys, zeros;  //分别记录1的位置和1前面的0的个数
    for(int i=0;i<s.length();i++){
        ch = s[i];
        if(ch == '1') {
            keys.push_back(i);
            zeros.push_back(zero_cnt);
            zero_cnt = 0;  //计数归零
        }
        else if(ch == '0') zero_cnt += 1;
    }
    if(ch == '0') zeros.push_back(zero_cnt);
    else zeros.push_back(0);

    if(k == 0){
        long long num;
        for(int i=0;i<zeros.size();i++){
            num = zeros[i];
            res += num * (num + 1) / 2;
        }
    }
    else if(keys.size() < k) res = 0;
    else{
        int i = 0, j = k-1;
        while(j < keys.size()){
            res += (zeros[i] + 1) * (zeros[j+1] + 1);
            i++; j++;
        }
    }
    cout << res;
    return 0;
}
发表于 2020-03-20 10:23:57 回复(4)

Golang 数学解法

这道题一入眼我首先想到的是双指针,但是仔细考虑一下发现左右指针轮流右移的思路行不通,如果一定要用双指针,需要右指针扫描完了回到起点再操作,这就和暴力法没什么区别了。
我想到另一种思路,创建一个数组pos[]记录每个1的位置。
我们结合一个例子来分析一下一般情形,针对如下的pos[]:

2 4 9 10 14 18

考虑第5个1,位置是14,假如k=3,第2个1的位置是4。
那么包含9,10,11这三个1的所有可能性可以表示为:
右侧滑动范围(14,18],左侧滑动范围((4,9],可能性有(18-14)*(9-4)=20种。
然后改变末位1的位置求得总共的可能性。

下附代码:

package main

import "fmt"

func main()  {
    var k,ans int
    var str string
    fmt.Scan(&k,&str)
    pos:= []int{-1}
    for i,v:=range(str){
        if v==49{    //"1"的ASCII码为49
            pos=append(pos, i)
        }
    }
    if k>=len(pos){
        fmt.Println(0)
        return
    }
    pos=append(pos, len(str))
    if k==0{
        for i:=k;i<len(pos)-1;i++{
            ans+=(pos[i+1]-pos[i]-1)*(pos[i+1]-pos[i])/2
        }
    }else {
        for i := k; i < len(pos)-1; i++ {
            ans += (pos[i+1] - pos[i]) * (pos[i-k+1] - pos[i-k])
        }
    }
    fmt.Println(ans)
}
编辑于 2020-04-05 16:25:05 回复(1)

枚举即可,查看题解请移步至 ABC

发表于 2024-06-23 14:09:32 回复(0)
//引用楼上思路:数出所有‘1’的左右各有多少0,
//然后第n个1的左边的0的个数为a,
//第n + k个1的右边的0的个数为b,
//那么这个组合有(a + 1) * (b + 1)种可能,
//将所有可能加起来就可以了。
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
#include<unordered_map>
using namespace std;
int main()
{
    int k; string s;
    while (cin >> k >> s)
    {
        int sum = 0;
        for (int i = 0; i < s.size(); i++)
        {
            if (s[i] == '1')
                sum++;
        }//计算1的个数
        if (sum < k)
        {
            cout << 0 << endl; break;
        }//特殊情况1的个数小于k
        vector<pair<int,int>> zeros(s.size()+1,pair<int,int>(0,0)); 
        long num = 0; long long res = 0;//结果必须用Long long 存储
        int i = 0;
        for (; i < s.size();i++)
        {
            if (s[i] == '1')//计算每个1右侧0的个数
            {
                //顺便计算k=0时的种类数,比如有三个连续0,则种类数位1+2+3=3*(1+3)/2
                zeros[i].first = num; res +=(long long)num * (num + 1) / 2;// cout << num;
                num = 0;
            }
            else
                num++;
        }
        //cout << endl;
        if(s[--i]=='0')//考虑循环中不是1结尾的情况
            res+=(long long)num * (num + 1) / 2;
        if (k == 0)
        {
            cout << res << endl; break;//k=0时特殊处理
        }
        num = 0;
        i = s.size() - 1;
        for (; i >=0; i--)//统计1的左侧0的个数
        {
            if (s[i] == '1')
            {
                zeros[i].second = num; //cout << num;
                num = 0;
            }
            else
                num++;
        }
        //cout << endl;
        res = 0;
        if (k == 1)//如果k等于1,则对于每个1,种类数等于左侧的0的个数+1与右侧0的个数+1的乘积
        {
            for (int i = 0; i < s.size(); i++)
            {
                if(s[i]=='1')
                    res+= (zeros[i].first + 1) * (zeros[i].second + 1);
            }
            cout << res << endl; break;
        }
        int left = 0; //如果k不等于1,则对于每个k个1,种类个数,等于最左侧的1左侧零的个数+1与最右侧1的右侧0的个数+1的乘积
        while (left < s.size() && s[left]!= '1')
            left++;
        int l = 0; int right = 0; bool kk = 0;
        while (right < s.size() && l < k)
        {
            if (s[right] == '1')l++; right++; kk = 1;
        }//此时left和right之间包含k个1,且分别为左右端点
        res = 0; if(kk)right--;
        while (left <= right && right < s.size())
        {
            if (l == k && s[left] == '1')//计算
                res += (zeros[left].first + 1) * (zeros[right].second + 1);
            if (l == k)//左侧移动到下一个1
            { 
                left++;
                while (s[left] != '1')
                    left++;
                l--;
            }
            else
                break;            
            if (l < k)//右侧移动到下一个1
            {
                right++;
                while (right<s.size()&&s[right] != '1')
                    right++;
                l++;
            }
        }
        cout << res << endl;
    }
    return 0;
}

发表于 2020-12-04 20:01:40 回复(0)

#include<iostream>
#include<string>
#include<vector>
using namespace std;
typedef long long ll;
ll get_res(string &s, int K) {
    ll res = 0;
    ll count = 0;
    vector<int> Index;
    Index.push_back(-1);
    for (int i = 0; i < s.size(); i++) {
        if (s[i] == '1') {
            count++;
            Index.push_back(i);
        }
    }
    if (count < K)return res;
    Index.push_back(s.size());
    count = 0;
    int i = 1;
    if (K == 0) {
        if(Index.size()==2)return ll((1+s.size())*s.size()/2);
        while (i  < Index.size()) {
            count += (1+Index[i]-Index[i-1]-1)*(Index[i] - Index[i - 1] - 1)/2;
            i++;
        }
        return count;
    }
    while (i + K - 1 < Index.size()-1) {
        int temp1 = Index[i] - Index[i - 1] - 1;
        count += 1+(Index[i]-Index[i-1]-1 + Index[i + K] - Index[i+K-1] - 1 + (Index[i] - Index[i - 1] - 1) * (Index[i + K] - Index[i+K-1] - 1));
        i++;
    }
    return count;
}
int  main(){
    int K;
    cin>>K;
    string s;
    cin>>s;
    ll res = get_res(s,K);
    cout<<res;
    return 0;
}



发表于 2020-05-26 21:46:14 回复(0)
通过率100%
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
int main(){
	int k;
	cin>>k;
	if(k==0){
		getchar();
		char c;
		long long Number0=0;
		long long sum=0;
		while((c=getchar())!=-1){
			if(c=='0'){
				Number0++;
			}else if(c=='1'){
				//cout<<Number0<<"个0"<<endl; 
				sum+=Number0*(Number0+1)/2; 
				Number0=0;
			}
			
		}
		sum+=Number0*(Number0+1)/2; 
			//cout<<Number0<<"个0"<<endl; 
		cout<<sum<<endl;
		return 0;
	}
	char buff[1000001];
	cin>>buff;
	
	int sum=0;//组合个数 
	int l=0;//左边的点,一般右自由度计算完毕,就用它计算左自由度,计算完之后再指向下一个节点的下一个方便下次计算 
	int r=0;
	int lNumb=0;//当前窗口左边的自由度
	int rNumb=0;//当前窗口右边的自由度
	int count=0;//当个数满足后,计算右边自由度,乘法即可求出数量 
	
	while(buff[r]!=0){
		
		if(buff[r]=='1'){//右边找到1时,如果++count==k,就开始计算右边自由度,如果>k说明自由度计算完毕 
		
			if(count==k){//已经计算完右边自由度,该计算左边自由度 
				do{ 
					lNumb++;
				}
				while(buff[l++]!='1');
				//cout<<"左边自由度"<<lNumb<<endl;
				//cout<<"右边自由度"<<rNumb<<endl;
				sum+=lNumb*rNumb;
				lNumb=0;
				rNumb=0;
				count--;
				continue; 
			}else{
				count++;
			}
		}
		if(count==k){//计算右边自由度,得到结果 
			 rNumb++;
		}
		r++;
	}
	if(count==k){
		
			do{
					lNumb++;
				}
				while(buff[l++]!='1');
				//cout<<"左边自由度"<<lNumb<<endl;
				//cout<<"右边自由度"<<rNumb<<endl;
				sum+=lNumb*rNumb;
	}
	cout<<sum<<endl;
	return 0;
} 
第一阶段,k!=0时,
使用滑动窗口的思想,确定窗口的左端点和右端点,然后计算左边的自由度和右边的自由度(自由度就是一侧的连续的0的个数加上1),如
k=1
str="1010"
滑动窗口第一次定位下标为0的点,左自由度=1,右自由度=2 ,所以该窗口下有1*2=2种可能。下一次定位.总的来看思路和上酸菜同学说的一样(我是做完之后才看到他的思路,我怕我说不清楚,大家可以看他的思路来理解,但代码写法思路类似该思路,细节不一样)
所以核心是计算出所有窗口的左自由度和右自由度即可。
本代码的计算方法是,窗口右指针向右滑动,当滑到1时,count计数+1.当计数完后发现count==k,就说明窗口确定了,此时计算右自由度。右指针一直滑动到下一个为1的节点,可以计算出右自由度(细节大家自己随便改改,主要思路懂了就行)
当右自由度计算完成,再来计算左自由度 ,计算方法是左指针指向上一个节点,然后开始移动,反正最终目的可以计算出左自由度。计算完毕,结果加上 左自由度乘右自由度就行了。

测试用例后期还会有一些小陷阱,就是当k=0时,给你一套例子来算,
再说一下当k=0时,计算的是0的排列方式,采取了另一套算法来计算。其实连续的0中子串的数量,就是C(2,n)+C(1,n),也就是1+2+3+4+...+n,所以用等差数列求和公式算就行啦。最后变量类型必须是long long,因为最后一个测试用例是7000多个0,用普通的int算不了(我觉得应该可以算,但实际上算不了,改成long long结果就对了)
完毕




编辑于 2020-05-26 15:49:05 回复(0)

我有一个想法:就是数出所有‘1’的左右各有多少0,然后第n个1的左边的0的个数为a,第n+k个1的右边的0的个数为b,那么这个组合有(a+1)*(b+1)种可能,将所有可能加起来就可以了。

比如说

输入

1
1010

输出

6

那么第一个1左边0个0,右边1个0,那么有12=2种符合条件的子串,第二个1左边1个0 ,右边1个0,有22=4种可能,总共2+4=6种可能。

发表于 2020-04-26 15:35:33 回复(0)

优化前面楼主的代码,子串匹配问题。子串要求有k个1,则创建一个k+1大小的数组,进行子串表示。(低n个字符是否能满足子串,当第n个字符为0时,跟前面k个1有关,当第n个字符为1时,跟前面k-1个1有关)。长度为k+1的数组status存放了相连的k+1个1后面0的个数。如第n个字符为子串的最后一个字符,则该子串必须包含第n字符以及前面相连最近的k个为1的字符,可以在字符的前面加0,每添加一个连续的0,则多一个子串的组合。

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int k = sc.nextInt();
        sc.nextLine();
        String str = sc.nextLine();
        char[] strChar = str.toCharArray();
        int[] status = new int[k + 2];
        status[0] = 1;
        int num = 0;
        long result = 0;
        for (char c : strChar) {
            if (c == '1') num++;
            if (c == '1') status[(num) % (k + 1)] = 0;
            if (num >= k) result+= status[(num - k) % (k + 1)];
            status[num % (k + 1)]++;
        }
        System.out.println(result);
    }
}
发表于 2020-04-20 13:00:36 回复(0)
import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int k = scan.nextInt();
        String s = scan.next();
        char[] arr = s.toCharArray();
        int count = 0, N = s.length(), map[] = new int[N + 2];
        long result = 0;
        map[0] = -1;
        for(int i = 0; i < N; i++) {
            char c = arr[i];
            // 当 K 为 0 的情况下
            if(c == '0') {
                if(k == 0) {
                    result += (i - map[count]);
                } else if(count >= k) {
                    result += map[count - k + 1] - map[count - k];
                }
            } else {
                map[++count] = i;
                if(count >= k && k != 0) {    // 这里 k!=0 因为当 k 为0, 则所有 1 直接失效了
                    result += map[count - k + 1] - map[count - k];
                }
            }
        }
        System.out.println(result);
    }
}
做法就是对于以第 i 个位结束点的子字符串,我们找到第 count - k + 1 的坐标和 count - k 的坐标之间的个数, 因为在 [count - k + 1,count -k] 这个区间内的所有值都能满足到 i 的1的个数是 k 个。当然这道题目必须要考虑 k 为 0 的情况, 这里也加入了。




发表于 2020-04-12 02:39:50 回复(0)
思路是通过满足题意的子数组来计算总和,k=0和非0要分开看。
不知道思路和代码对不对,通过了94%。错误案例是0 和字符串全0(长度目测应该接近1W),但是在自己编译器没有问题。(直接暴力解有75%,讲真没时间暴力解就完事了,这题花了半个多小时也没过,很难受)
using namespace std;
#
include<vector>
int main(){
    int k;
    string s;
    while(cin>>k>>s){
     vector<int> res;
        int count=0;
            for(auto i:s){
            if(i=='0') count++;
            else{
              res.push_back(count);
                count=0;
            }
        }
        res.push_back(count);
        if(k == 0){               //k=0
             long sum=0;
            for(int i=0;i<res.size();i++){
               long temp=(res[i]*(res[i]+1))>>1;
                sum+=temp;
            }
            cout<<sum<<endl;
        }
        else{              //k!=0
             int sum=0;
            int first=0;
            int last=0;
           while(last<res.size()){        //核心代码
                last++;
                while(last<res.size()&&last>first&&last-first==k){
                    sum+=(res[last]+1)*(res[first]+1);
                    first++;
                }
            }
        cout<<sum<<endl;
        } 
    }
    return 0;
}
发表于 2020-03-19 00:41:35 回复(5)