[SMOJ1829]公司招聘

97 篇文章 0 订阅
8 篇文章 0 订阅

题目描述

某著名IT公司开始招聘啦!你是否精通JAVA?是否精通C++?是否精通HTML?是否精通MYSQL?是否精通PYTHON?。。。。。
该公司要考察总共 K 门技术,技术编号从 1 至 K
N 名学生应聘,学生编号从 1 至 N
i 名学生精通的技术集合是 seti,表示的意义是:把十进制整数 seti 展开成二进制后,从右往左看该二进制数,如果第 i 位是 1 表示精通第 i 门技术,否则不精通第 i 门技术。
例如某名学生精通的技术集合 set=11,那么把11展开成二进制是 1011,从右往左看该二进制数,表示该学生精通第 1、第 2、第 4 共三门技术,不精通第 3 门技术。
现在把这 N 名学生从左往右排成一行,知道了每个学生精通的技术集合,现在公司决定录取一段编号连续的学生,但是该段连续的学生必须满足:
1、记该段连续的学生,精通第 1 门技术的,共有 a1人。
2、记该段连续的学生,精通第 2 门技术的,共有 a2 人。
3、记该段连续的学生,精通第 3 门技术的,共有 a3 人。
。。。。。。
K 、记该段连续的学生,精通第 K 门技术的,共有 aK 人。
那么必须满足 a1=a2=a3==aK

现在的问题是:在满足上述条件下,公司最多能录取多少学生?注意:被录取的学生必须是编号连续的一段学生。

输入格式 1829.in

第一行,两个整数, N K 1N100000 , 1K30
第二行, N 个整数,第 i 个整数是 seti ,表示第 i 个学生精通的技术的集合。

输出格式 1829.out

一个整数。

输入样例 1829.in

7 3
7 6 7 2 1 4 2

输出样例 1829.out

4

【样例解释】

第 3 至第 6 名学生满足题意,因为该段连续编号的学生,共有 2 人精通第 1 门技术,有 2 人精通第 2 门技术,有 2 人精通第 3 门技术。


题意:给定 n 个二进制下不超过 k 位的整数 ai,要求找到最长的一段 [l,r] ,满足 al ar 这一段数的二进制表示中,每一位 1 的个数恰好相同。

例如对于 n=7 k=3 ,整数分别为 7 6 7 2 1 4 2 时,最优方案是选取 3 到 6 这一段,因为把它们拆成二进制:

7 = 111
2 = 010
1 = 001
4 = 100

每一位 1 的个数都是 2 个,符合要求。

最显然的做法就是枚举 l r,然后统计每一位的 1 ,逐一检查相邻列,但是这样太暴力了,时间复杂度高达 O(n3×k) ,有没有不那么暴力的方法呢?
当然是有的,其实很多人都已经在赛场上想到了这个优化。既然是要统计连续一段的一个和,显然可以搞个前缀和维护一下,这样就不用每次都统计每一位的 1 了,而是可以在 O(1) 时间内算出 [l,r] 这一段某一列的 1 个数。很遗憾,这个做法也是 O(n2×k) 的,无法解决我们的问题。

代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int maxn = 1e5 + 10;
const int maxk = 30 + 5;

int n, k;
int set[maxn];
int presum[maxn][maxk];

int cnt[maxn];
bool check(int l, int r) {
    cnt[0] = presum[r][0] - presum[l - 1][0];
    for (int i = 1; i < k; i++) {
        cnt[i] = presum[r][i] - presum[l - 1][i];
        if (cnt[i - 1] != cnt[i]) return false;
    }
    return true;
}

int main(void) {
    freopen("1829.in", "r", stdin);
    freopen("1829.out", "w", stdout);

    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) scanf("%d", &set[i]);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < k; j++)
            presum[i][j] = presum[i - 1][j] + (bool)(set[i] & (1 << j));

    for (int len = n; len >= 1; len--)
        for (int start = 1; start + len <= n + 1; start++)
            if (check(start, start + len - 1)) { printf("%d\n", len); return 0; }
    puts("0");
    return 0;
}


不妨从前缀和的角度入手,观察一下写成前缀和形式时,满足条件的 [l,r] 的特征:

原数:
111
110
111
010
001
100
010
前缀和:
111
221
332
342
343
443
453

不难看出,对于满足条件的 [l,r] ,每一位的前缀和从 l1 r 增量是一样的。(注意一下为什么是 l1,跟一维前缀和是一样的)
换言之,从某种相对意义上而言,到 r 的前缀和可以看作到 l1 的前缀和的一个偏移。再仔细观察,可以发现,相邻两列的差是一样的。

于是这就可以作为一种标识。对于第 i 行,计算出到它的前缀和,并得到相邻两列的差,然后用 hash 往前查找是否存在某一行,使得这两行的前缀和相邻两列的差相同。如果有多行满足,则根据贪心策略,应该取最前面的一行。计算得出答案后再将当前行 hash 一下,标记。考虑到可能会有冲突,但是机率不大,可以用拉链法。

但是要注意,必须在最前面加一行差全为 0 作为边界,否则如果从第一行开始的就会被算错。

这样一来,就可以在 O(n×k) 的时间内解决本题了。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

const int maxn = 1e5 + 10;
const int maxk = 30 + 5;
const int prime = 1e6 + 7;

int n, k;
int set[maxn];
int presum[maxn][maxk]; //前缀和
int diff[maxn][maxk]; //相邻两列差

vector <int> hash[prime];

bool issame(int x, int y) { //出现冲突暴力判断
    for (int i = 0; i + 1 < k; i++)
        if (diff[x][i] != diff[y][i]) return false;
    return true;
}

int find(int num, int *arr) {
    int val = 0;
    for (int i = 0; i + 1 < k; i++) val = (val  + arr[i]) % prime*107LL %prime; //参考字符串 hash
    hash[val].push_back(num);
    int len = hash[val].size(); int pos = num; //至少可以录用一个人,取自己本身作初值
    for (int i = 0; i < len; i++)
        if (issame(num, hash[val][i])) pos = min(pos, hash[val][i]); //贪心取最前一个
    return pos;
}

int main(void) {
    freopen("1829.in", "r", stdin);
    freopen("1829.out", "w", stdout);

    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) scanf("%d", &set[i]);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < k; j++)
            presum[i][j] = presum[i - 1][j] + (bool)(set[i] & (1 << j)); //强制类型转换计算前缀和

    int ans = 0;
    for (int j = 1; j < k; j++) diff[0][j - 1] = 100000; //注意 0 也要加上偏移量
    find(0, diff[0]); //边界
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j < k; j++) diff[i][j - 1] = presum[i][j] - presum[i][j - 1] + 100000;
        ans = max(ans, i - find(i, diff[i])); //find 返回的结果是 l-1,因此长度正好为 r 减去返回值,不用加 1
    }
    printf("%d\n", ans);
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值