题目描述
某著名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 。 1≤N≤100000 , 1≤K≤30 。
第二行, 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=7 , k=3 ,整数分别为 7 6 7 2 1 4 2 时,最优方案是选取 3 到 6 这一段,因为把它们拆成二进制:
7 = 111
2 = 010
1 = 001
4 = 100
每一位 1 的个数都是 2 个,符合要求。
最显然的做法就是枚举
l
和
当然是有的,其实很多人都已经在赛场上想到了这个优化。既然是要统计连续一段的一个和,显然可以搞个前缀和维护一下,这样就不用每次都统计每一位的 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]
,每一位的前缀和从
l−1
到
r
的增量是一样的。(注意一下为什么是
换言之,从某种相对意义上而言,到
r
的前缀和可以看作到
于是这就可以作为一种标识。对于第 i 行,计算出到它的前缀和,并得到相邻两列的差,然后用 hash 往前查找是否存在某一行,使得这两行的前缀和相邻两列的差相同。如果有多行满足,则根据贪心策略,应该取最前面的一行。计算得出答案后再将当前行 hash 一下,标记。考虑到可能会有冲突,但是机率不大,可以用拉链法。
但是要注意,必须在最前面加一行差全为 0 作为边界,否则如果从第一行开始的就会被算错。
这样一来,就可以在
参考代码:
#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;
}