二分
1. 最佳牛围栏
来源:《算法竞赛进阶指南》
原题链接
原题链接
题目描述
农夫约翰的农场由 \(N\) 块田地组成,每块地里都有一定数量的牛,其数量不会少于 \(1\) 头,也不会超过 \(2000\) 头。
约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。
围起区域内至少需要包含 \(F\) 块地,其中 \(F\) 会在输入中给出。
在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。
输入格式
第一行输入整数 \(N\) 和 \(F\),数据间用空格隔开。
接下来 \(N\) 行,每行输入一个整数,第 \(i+1\) 行输入的整数代表第 \(i\) 片区域内包含的牛的数目。
输出格式
输出一个整数,表示平均值的最大值乘以 \(1000\) 再 向下取整 之后得到的结果。
数据范围
\(1 \le N \le 100000\)
\(1 \le F \le N\)
输入样例
10 6
6
4
2
10
3
8
5
9
4
1
输出样例
6500
算法:(二分 + 前缀和 + 双指针) \(O(n)\)
算法内容:
-
为什么可以二分\(avg\)?
因为我么要找的是最大的\(avg\),其具有二段性 -
如何写\(check\)函数?
\(check\)函数表示有没有一段长度大于等于\(F\)的区间,使这段区间的平均数尽可能的大,如果我们找到了这样的的区间且这段区间的平均数大于我们二分的平均数,则返回\(true\),否则返回\(false\)。
那么如何找到一段长度大于等于F的区间,使这段区间的平均数尽可能大呢?
首先我们要知道:对于一段序列,让每个数都减去我们算的平均数后。如果这段序列的和大于\(0\),则说明这段序列的平均值比我们算的平均值大,反之小。我们枚举区间的右端点\(k\),每一个右端点对应的区间为\(1\)~\(k\), \(2\)~\(k\), \(3\)~\(k\), \((k-F+1)\)~\(k\)。所以问题转化为是否存在一段区间使得\(max(s[k] - s[l - 1]) \ge 0\),即\(s[r] - min(s[l - 1]) \ge 0\)。那么如何找出\(min(s[l - 1])\)呢?我们可以用一个变量\(mins\)来不断更新
C++代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, F;
double a[N], s[N];
bool check(double avg)
{
for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i] - avg;
double mins = 0;
for (int k = F; k <= n; k ++ )
{
mins = min(mins, s[k - F]);
if (s[k] >= mins) return true;
}
return false;
}
int main()
{
scanf("%d%d", &n, &F);
double l = 0, r = 0;
for (int i = 1; i <= n; i ++ )
{
scanf("%lf", &a[i]);
r = max(r, a[i]);
}
while (r - l > 1e-5)
{
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%d\n", (int)(r * 1000));
return 0;
}
2. 分巧克力
第八届蓝桥杯省赛C++A/B组,第八届蓝桥杯省赛JAVAA/B组
原题链接
题目描述
儿童节那天有 \(K\) 位小朋友到小明家做客。
小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 \(N\) 块巧克力,其中第 \(i\) 块是 \(H_i×W_i\) 的方格组成的长方形。
为了公平起见,小明需要从这 \(N\) 块巧克力中切出 \(K\) 块巧克力分给小朋友们。
切出的巧克力需要满足:
- 形状是正方形,边长是整数
- 大小相同
例如一块 \(6×5\) 的巧克力可以切出 \(6\) 块 \(2×2\) 的巧克力或者 \(2\) 块 \(3×3\) 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?
输入格式
第一行包含两个整数 \(N\) 和 \(K\)。
以下 \(N\) 行每行包含两个整数 \(H_i\) 和 \(W_i\)。
输入保证每位小朋友至少能获得一块 \(1×1\) 的巧克力。
输出格式
输出切出的正方形巧克力最大可能的边长。
数据范围
\(1≤N,K≤105\),
\(1≤H_i,W_i≤105\)
输入样例
2 10
6 5
5 6
输出样例
2
算法:(二分)\(O(nlogn)\)
算法内容
- 假设边长为\(x\),则切出来的巧克力总块数为
-
为什么可以二分?
题目要求的的是满足切出来的巧克力块数\(cnt>=k\)的最大的边长是多少。
假设答案为\(ans\),则小于\(ans\)的所有边长都满足\(cnt>=k\),因为\(ans\)是满足条件的最大边长,所以大于等于\(ans\)的所有边长都满足\(cnt<k\),满足二段性,可以二分 -
时间复杂度分析:二分的复杂度时\(O(logn)\),\(check\)函数复杂度是\(O(n)\),总时间复杂度为\(nlogn\)
C++代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, k;
int h[N], w[N];
bool check(int mid)
{
LL cnt = 0;
for (int i = 0; i < n; i ++ )
{
cnt += (LL)(h[i] / mid) * (w[i] / mid);
if (cnt >= k) return true;
}
return false;
}
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i ++ ) scanf("%d%d", &h[i], &w[i]);
int l = 0, r = 1e5;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
3. 最长公共子串
来源:上海交通大学考研机试题
原题链接
题目描述
给定两个字符串,求这两个字符串的不包含数字的最长公共子串的长度。
输入格式
共两行,每行一个由小写字母和数字构成的字符串。
输出格式
一个整数,表示给定两个字符串的不包含数字的最长公共子串的长度。
如果不存在满足要求的非空公共子串,则输出 \(0\)。
数据范围
输入字符串的长度均不超过 \(10000\)。
输入样例
ab123abccff
abcfacb123
输出样例
3
算法:(二分 + 字符串哈希)\(O(nlogn)\)
算法内容
-
为什么可以二分?
假设答案为\(ans\),则\(ans\)为公共子串的最大长度,当长度小于\(ans\)时,两个串一定存在公共子串;当长度大于\(ans\)时,两个串一定不存在公共子串,所以答案具有二段性,可以进行二分。 -
\(check\)函数怎么写?
每一次二分,我们可以将第一个串中所有长度为\(mid\)的子串插入哈希表中,然后在第二个子串中枚举所有长度为\(mid\)的子串,查询它是否在哈希表中出现,如果出现了则证明有公共子串 \(return true\),否则没有 \(return false\) -
如何优化哈希的方式?
如果我们用普通的哈希方式,我们每一次二分最多需要像哈希表中插入\(n\)个长的为\(mid\)的字符串,我们需要存的总字符数最多是\(nm\)个,时间复杂度为\(O(n^2)\),加上二分的复杂度,总时间复杂度为\(O(n^2logn)\),会\(TLE\)
所以我们可以采取字符串哈希的方法:我们可以通过\(O(n)\)的预处理,用\(O(1)\)的时间求出任何一个子串的哈希值。我们把任何一个字符串都变成一个整数,所以我们在向哈希表中插入时,只需要插入\(n\)个整数,时间复杂度优化为\(O(n)\),加上二分的复杂度,总时间复杂度为\(O(nlogn)\) -
如何实现所求的公共子串不包含数字?
我们把第一个字符串中的数字变为'#',把第二个字符串中的数字变为 '@' ,因为'#'只在第一个字符串中出现,'@'只在第二个字符串中出现,这样处理完后,在\(check\)函数判断时就不会把包含数字的公共子串判断为\(true\)
C++代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
using namespace std;
typedef unsigned long long ULL;
const int N = 20010, base = 131;
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
bool check(int mid) //判断两个串是否含有长度为mid的子串
{
unordered_set<ULL> hash;
//将第一个字符串的所有长度为mid的子串的哈希值存到hash表中
for (int i = 1; i + mid - 1 <= n; i ++ )
hash.insert(get(i, i + mid - 1));
//枚举第二个字符串
for (int i = n + 1; i + mid - 1 <= n + m; i ++ )
if (hash.count(get(i, i + mid - 1)))
return true;
return false;
}
int main()
{
scanf("%s", str + 1);
n = strlen(str + 1);
//将两个字符串拼接起来
scanf("%s", str + n + 1);
m = strlen(str + n + 1);
//预处理字符串哈希值
p[0] = 1;
for (int i = 1; i <= n + m; i ++ )
{
p[i] = p[i - 1] * base;
char c = str[i];
if (isdigit(c))
{
if (i <= n) c = '#';
else c = '@';
}
h[i] = h[i - 1] * base + c;
}
//二分答案
int l = 0, r = min(n, m);
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}