在前缀和上进行二分
题目描述:
有一个数组,数组中的数字可以是红色或者蓝色。
找到一个子数组,子数组内,红色的数乘积为x,蓝色的乘积为 y,并且 x 末尾 0 的数量和 y 末尾 0 的数量总和不小于 k,找到满足这样要求的子数组长度的最小值。
输入示例:
1 4 15 8 5 BRRBB
输出示例:
4
思路:
-
看到 末尾 0 的个数,就要想到 质因子 2/5 的对数,因为 0 是由某个数乘 10 产生的。
-
这个题又要求每个子数组内乘积末尾 0 的个数,为了加快查找速度,可以使用前缀数组进行记录,这样就能在 O ( 1 ) O(1) O(1) 的时间内,得到区间内质因子 2/5 的对数了。
-
暴力的话,还是需要 O ( n 2 ) O(n^2) O(n2) 的时间来得到每个子数组,有什么方法可以加速查找呢?二分,所要求的最长满足题意的长度为 n,最短的为0,当一个长度 mid 满足要求的时候,可以暂时先将这个长度记为答案,然后缩小区间,为什么?因为长度大于 mid 也一定是满足要求的,就没必要再求了,所以也相当于一次排除了一半的选项。
整个代码如下,其中包含了对数器进行测试。
public class ZeroMax {
public static void main(String[] args) {
int[] nums;
String color;
int testTime = 1000000;
int maxVal = (int) 1e9;
int maxLen = 100;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
GeneInfo info = geneArr(maxLen, maxVal);
nums = info.nums;
color = info.color;
int k = (int) (Math.random() * 20) + 1;
genePerfix(nums, color);
int res1 = median(nums.length, k);
int res2 = baoli(nums.length, k);
if (res1 != res2) {
System.out.println("出错了");
}
}
System.out.println("测试结束");
}
public static void print2DArray(int[][] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print("row " + i + ": ");
for (int j = 0; j < arr[0].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
System.out.println();
}
// 第 0 行代表 2 的前缀数组
// 第 1 行代表 5 的前缀数组
static int[][] B25;
static int[][] R25;
//生成前缀数组
public static void genePerfix(int[] nums, String color ) {
B25 = new int[2][nums.length + 1];
R25 = new int[2][nums.length + 1];
for (int i = 0; i < nums.length; i++) {
// 如果是蓝色的话,放进蓝色的前缀数组里面
// 得到当前的数有多少个 25
int[] cnts = get25(nums[i]);
if (color.charAt(i) == 'B') {
// 记录 2 的前缀和
B25[0][i+1] = B25[0][i] + cnts[2];
B25[1][i+1] = B25[1][i] + cnts[5];
R25[0][i+1] = R25[0][i];
R25[1][i+1] = R25[1][i];
} else {
R25[0][i+1] = R25[0][i] + cnts[2];
R25[1][i+1] = R25[1][i] + cnts[5];
B25[0][i+1] = B25[0][i];
B25[1][i+1] = B25[1][i];
}
}
}
// 对一个数,求它 2 和 5 质因子的数目
public static int[] get25 (int num) {
int begin = 2;
int[] res = new int[6];
while (num > 1) {
while ((num % begin) == 0) {
res[begin]++;
num /= begin;
}
begin++;
if (begin > 5) return res;
}
return res;
}
// 二分的查找方法,二分的是长度
// 根据题意要求,要求最短的,当某个长度满足要求的情况下,那么就要缩小区间
public static int median(int n, int k) {
// 最短长度可以是 0
int l = 0;
int r = n;
int res = Integer.MAX_VALUE;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (isValid(mid, n, k)) {
// 当前这个长度是满足要求的,存下结果,那么试一下更小的长度呢
res = Math.min(res, mid);
r = mid - 1;
} else {
l = mid + 1;
}
}
return res;
}
public static boolean isValid(int len, int maxLen, int k) {
// 判断长度为 len 的是否满足题意的要求
for (int i = len; i <= maxLen; i++) {
// 该区间范围内, 质因子 2 的个数
int Bnum2 = B25[0][i] - B25[0][i-len];
// 该区间范围内, 质因子 5 的个数
int Bnum5 = B25[1][i] - B25[1][i-len];
int Rnum2 = R25[0][i] - R25[0][i-len];
int Rnum5 = R25[1][i] - R25[1][i-len];
int B0 = Math.min(Bnum2, Bnum5);
int R0 = Math.min(Rnum2, Rnum5);
if ((B0 + R0) >= k) return true;
}
return false;
}
// 暴力解法,对每一个子数组,看其是否满足要求,如果满足要求,更新结果,得到最小区间长度
public static int baoli (int n, int k) {
int res = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
// 对每一个子数组做判断
// 左边界 i, 右边界 j
int Bnum2 = B25[0][j+1] - B25[0][i];
int Bnum5 = B25[1][j+1] - B25[1][i];
int Rnum2 = R25[0][j+1] - R25[0][i];
int Rnum5 = R25[1][j+1] - R25[1][i];
// Blue 结尾 0 的个数
int B0 = Math.min(Bnum2, Bnum5);
// Red 结尾 0 的个数
int R0 = Math.min(Rnum2, Rnum5);
// 当前范围内的数字
if ((B0 + R0) >= k) {
res = Math.min(res, j-i + 1);
}
}
}
return res;
}
static class GeneInfo {
String color;
int[] nums;
public GeneInfo(String color, int[] nums) {
this.color = color;
this.nums = nums;
}
}
// 生成随机数组,进行测试
public static GeneInfo geneArr(int maxLen, int maxVal) {
int len = (int)(Math.random() * maxLen + 1);
StringBuilder color = new StringBuilder();
int[] nums = new int[len];
for (int i = 0; i < len; i++) {
nums[i] = (int)(Math.random() * maxVal + 1);
color.append(Math.random() < 0.5 ? 'B': 'R');
}
return new GeneInfo(color.toString(), nums);
}
}
其中有一个感觉可以改进的地方是,得到一个数的 2/5 质因子的个数,还请懂得大佬不吝赐教。
还有一个类似的题目:
前缀和与二分查找的应用 | 编程之禅