二分
基本框架
二分查找的思维确实非常简单 就是引入一个猜炸弹编号的问题 相信大家玩过对吧 假如炸弹是0-100中间的一个数字 不会玩的人可能一个一个猜 会玩的人一般上来就猜50 25 …
二分查找代码中的细节很重要但是真正的坑根本就不是那个细节问题,而是在于到底要给 mid 加一还是减一,while 里到底用 <= 还是 <,
这就要分别对应两种写法 :
一种是左闭右闭区间 一种是左闭右开区间 讲解视频我附在这里:
代码随想录的详细讲解
labuladong 算法笔记 :
整数查找(序列二分的模版题 建议先做)
满分代码及思路
首先题目其实说的非常直白就是根据给的flag(1 2 3 4 )分别对应四种不同的查询方案 那么我们的main函数就分别对于这四种情况 分别调用函数返回结果即可
做完你再看 序列二分的本质上 是不是 在有限的一个区间或者序列中 快速找到满足条件的数
solution
#include <iostream>
using namespace std;
int n, q;
const int N = 1e5 + 9;
int a[N]; // 存一下序列A
int flag; // 取决于我们该怎么去查找
int l, r, x;
int ans;
// 输出 A[l∼r] 中等于 x 最左边的数的下标,若不存在输出 -1
void search1(int l, int r, int x) {
ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] == x) {
ans = mid;
r = mid - 1;
} else if (a[mid] < x) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
// 输出 A[l∼r] 中等于 x 最右边的数的下标,若不存在输出 -1
void search2(int l, int r, int x) {
ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] == x) {
ans = mid;
l = mid + 1;
} else if (a[mid] < x) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
// 输出 A[l∼r] 中大于等于 x 的第一个数的下标,若不存在输出 -1
void search3(int l, int r, int x) {
ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
}
// 输出 A[l∼r] 中大于 x 的第一个数的下标,若不存在输出 -1
void search4(int l, int r, int x) {
ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] > x) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
}
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
while (q--) {
cin >> flag >> l >> r >> x;
if (flag == 1) {
search1(l , r , x);
} else if (flag == 2) {
search2(l , r , x);
} else if (flag == 3) {
search3(l , r , x);
} else if (flag == 4) {
search4(l , r , x);
}
cout << ans << endl;
}
return 0;
}
子串简写
满分代码及思路
solution 1(暴力 模拟双指针70分)
对于这道题目其实我首先想到的就是双指针,具体拿这题来说就是,我们现在有两个指针left right 现在要做的就是 在给定的字符串中枚举出所有同时满足:
1.长度大于等于K;
2.以c1开头以c2结尾
以上两个条件的所有子串 对吧
那么就很好办了呀 我们先让left指针找到c1,然后通过不断移动right
枚举出所有以left此时位置为左端点 且满足条件的所有合法情况 每次更新答案count 就好 最后输出一下
但是有几个比较大的样例超时了 这时候我们就需要想办法优化一下:
因为我们这个双指针的方法 主要依靠两个for循环 时间复杂度是O(n*n)
Q:所以我们要思考的是我们可以去优化的点在哪啊?
也就是说这个代码有什么重复且不必要的操作吗?
我觉得这个思考很重要很重要
A:二分相对于双指针优化的点是不是在于c2位置的确定啊 因为双指针需要枚举right每一种情况吧 因为我们就像一个瞎子 我们的right只有走到它的儿子面前才能跟他相认
具体来说,在双指针方法里,对于每一个以 c1 开头的位置,代码需要从 left + K - 1 开始,逐个枚举所有可能的 right 位置,直到找到以 c2 结尾的位置,从而判断是否满足子串长度大于等于 K 的条件
二分查找方法首先记录下所有 c1 和 c2 出现的位置。对于每一个 c1 出现的位置,要寻找满足子串长度大于等于 K 的 c2 位置时,它利用 c2 位置数组的有序性(因为位置是按顺序记录的)进行二分查找
此时我们的right指针他是一个视力很好的人 他知道自己的子女 在这条路的哪个方位
二分查找每次将搜索范围缩小一半,其时间复杂度为 (O(log m)),其中 m 是 c2 出现的次数。对于所有 c1 出现的位置都进行二分查找,整体时间复杂度就是 (O(n log m)),在一般情况下可以近似看作 (O(n log n)),相比于双指针方法的 (O(n^2)) 有了显著的优化。
#include <iostream>
#include <string>
// 引入标准库命名空间
using namespace std;
// 双指针方法
int double_point(int K, const string& S, char c1, char c2) {
int n = S.length();
int count = 0;
for (int left = 0; left < n; ++left) {
if (S[left] == c1) {
for (int right = left + K - 1; right < n; ++right) {
if (S[right] == c2) {
++count;
}
}
}
}
return count;
}
int main() {
int K;
string S;
char c1, char c2;
// 读取输入
cin >> K;
cin >> S >> c1 >> c2;
// 计算结果
int result = double_point(K, S, c1, c2);
// 输出结果
cout << "双指针方法结果: " << result << endl;
return 0;
}
solution 2(二分 AC)
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
// 二分查找方法
int binary_search(int K, const string& S, char c1, char c2) {
int n = S.length();
vector<int> start;
vector<int> end;
// 记录 c1 和 c2 出现的位置
for (int i = 0; i < n; ++i) {
if (S[i] == c1) {
start.push_back(i);
}
if (S[i] == c2) {
end.push_back(i);
}
}
int count = 0;
for (int s : start) {
// 二分查找满足条件的 c2 位置
int left = 0, right = end.size() - 1;
int target = s + K - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (end[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
count += end.size() - left;
}
return count;
}
int main() {
int K;
string S;
char c1, c2;
// 读取输入
cin >> K;
cin >> S >> c1 >> c2;
// 计算结果
int result = binary_search(K, S, c1, c2);
// 输出结果
cout << "二分查找方法结果: " << result << endl;
return 0;
}
管道
满分代码及思路
代码和思路参考:视频讲解
BZW :这是我偶然刷到的一个up 真的很优秀很有思维
样例解释与思路分析
对于这道题目首先我们需要搞清楚 我们要干嘛
可以将这个管道看成一条线段 阀门就是线段上的点
我们需要明确的是这个阀门只要一打开就会向左右两边同时灌水 就可以看成是以这个点为中心向左右两边延伸 length的长度
并且 len与时间t的函数是一个单调递增函数
solution
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
int n,len;
const int N=1e5+10;
int a[N][2];//存储输入每个阀门的位置和时间
struct door{
int L;//位置
int S;//时刻
}d[N];
struct line{
int l;
int r;
}w[N];
//sort比较函数 从小到大排左端点 如果一样就把右端点大的放后面
bool cmp(line A,line B)
{
if(A.l!=B.l)
{
return A.l<B.l;
}else{
return A.r<B.r;
}
}
bool check(int x)
{
int cnt=0;
for(int i=0;i<n;i++)//生成区间
{
if(x<d[i].S)continue;
w[cnt].l=d[i].L-(x-d[i].S);
w[cnt].r=d[i].L+(x-d[i].S);
cnt++;
}
sort(w,w+cnt,cmp);
int last=0;
//线段合并
for(int i=0;i<cnt;i++)
{
//为什么是last+1 因为题目把这些管道看作是一个个离散的点 所以只要我们新区间的右端点>last+1(l=临界情况)
//就return false
if(w[i].l>last+1)
{
return false;
}
last=max(w[i].r,last);
}
if(last<len)
{
return false;
}
return true;
}
int main()
{
cin>>n>>len;
for(int i=0;i<n;i++)
{
cin>>d[i].L>>d[i].S;
}//读入完成
int mid=0;
int l=0;
int r=2e9;//最坏的情况就是从1ee9开始流 直到2e9才能铺满管道
//二分答案 左闭右开写法
while(l<r)
{
mid=(l+r)/2;
if(check(mid))//如果mid这个时间能铺满整个管道 说明在mid右边的时刻都是合法的
//那么我们就需要操作:
//不减1是因为我们不能保证mid是不是临界点 也就是我们不知道mid-1是什么情况
{
r=mid;
}else{
l=mid+1;
}
}
cout<<l<<endl;
return 0;
}
最大通过数
满分代码及思路
solution 1(贪心 50分)
// 我们现在需要做的就是用这K个水晶 通过尽可能多的关卡 并且我们不需要关心水晶的分配问题
// 也就是说两个人共享一个背包 首先在闯关之前 我肯定想要比较一下左右两个入口
// 优先通过消耗水晶小的关卡 这样才能保证 最好通过关卡的数量最多
#include <iostream>
using namespace std;
const int N = 2e5 + 9;
int a[N], b[N]; // 分别表示左右两边关卡的能源数量
int n, m, k; // 初始状态下有n+m道关卡 一共有K个水晶
int passCount;
void work() {
int i = 0, j = 0;
// 先处理左右两边都有的关卡
while (i < n && j < m) {
if (a[i] <= b[j] && k >= a[i]) {
passCount++;
k -= a[i];
i++;
} else if (k >= b[j]) {
passCount++;
k -= b[j];
j++;
} else {
break;
}
}
// 处理左边剩余的关卡
while (i < n && k >= a[i]) {
passCount++;
k -= a[i];
i++;
}
// 处理右边剩余的关卡
while (j < m && k >= b[j]) {
passCount++;
k -= b[j];
j++;
}
}
int main() {
cin >> n >> m >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int j = 0; j < m; j++) {
cin >> b[j];
}
work();
cout << passCount;
return 0;
}
问题
solution 2(二分 AC)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 4;
ll a[N], b[N], pre_a[N], pre_b[N];
ll n, m, k;
// 检查是否可以通过 mid 个关卡
bool check(ll mid) {
bool ok = false; // 记录 mid 关卡数是否有可行的方案,不关心 a,b 各通过几关
// 从关卡数少的人开始枚举(mid > max(m,n) 时)
for (ll i = 0; i <= min(min(m, n), mid); ++i) {
// 保证另外一个人有足够的关卡数来凑为 mid 关,没有就跳过
if (mid - i > max(m, n)) continue;
// 从关卡数少的人开始枚举(保证不越界)
if (m <= n && pre_b[i] + pre_a[mid - i] <= k) {
ok = true;
} else if (n < m && pre_a[i] + pre_b[mid - i] <= k) {
ok = true;
}
}
return ok;
}
// 解决问题的函数
void solve() {
cin >> n >> m >> k;
// 读取左边入口每个关卡所需的宝石数
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
// 读取右边入口每个关卡所需的宝石数
for (int i = 1; i <= m; ++i) {
cin >> b[i];
}
// 计算 a[i] 前缀和,代表 a 通过 i 关所需的宝石
for (int i = 1; i <= n; ++i) {
pre_a[i] = pre_a[i - 1] + a[i];
}
// 计算 b[i] 前缀和,代表 b 通过 i 关所需的宝石
for (int i = 1; i <= m; ++i) {
pre_b[i] = pre_b[i - 1] + b[i];
}
// 二分查找最大通过的关卡数
ll l = 0, r = m + n + 10;
while (l + 1 != r) {
ll mid = (l + r) >> 1;
if (check(mid)) {
l = mid; // mid 可行,说明 mid 可能可以更大,更新 l
} else {
r = mid;
}
}
cout << l; // l 为答案
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}