题目描述
思路
其实就是思维题,题意很简单,找到一个居民左侧和右侧(如果存在的话)的垃圾桶中最近的那个垃圾桶,然后让那个垃圾桶的计数器加一。
从题目中挖掘的性质:
- 左侧的垃圾桶就是小于当前位置(遍历到的居民的位置)中位置最大的那个
- 右侧的垃圾桶就是大于当前位置中位置最小的那个。
由于题目给定垃圾桶的位置升序(即使无需我们也可以排序),很容易想到二分,我们只需要枚举每个居民,然后两次二分来分别找到左侧和右侧的垃圾桶的位置就可以了。
二分为
O
(
l
o
g
n
)
O(log_n)
O(logn),再乘上需要枚举的居民个数
O
(
N
)
O(N)
O(N) 就是总的时间复杂度
但是这里有一个优化(用一次二分)和细节问题(二分边界处理–哨兵)。
一次二分:
由于右侧的那个垃圾桶是居民右侧位置最小的垃圾桶,那么该垃圾桶的前一个垃圾桶一定是居民左侧位置最大的垃圾桶(如果存在的话)。
这个优化相当重要,这也是我们使用双指针代替二分实现变形复杂度的基础。
哨兵
前面一直提到,如果垃圾桶存在的话。是因为存在两种边界情况:(题目给定垃圾桶至少一个。)
- 一个居民位于所有垃圾桶的左侧,因此不存在它右侧的垃圾桶。
- 一个居民位于所有垃圾桶的右侧,因此不存在它左侧的垃圾桶。
所以说为了二分的时候简单一些,我们可以分别在最左侧和最右侧设置两个哨兵。
但是这两个哨兵的位置该取多少呢?这个最好不要凭空想象。
我们可以从二分的语句里面去观察:由于我们需要比较左侧垃圾桶和右侧垃圾桶到居民的距离那个更小。
即:
r
i
g
h
t
−
c
u
r
right - cur
right−cur 与
c
u
r
−
l
e
f
t
cur - left
cur−left 谁的差值更小。
当我们处于边界情况的时候,我们肯定不希望取到我们的哨兵。所以说:
- 处于边界情况 1 1 1,即不存在右侧的垃圾桶,此时 r i g h t right right 取得哨兵的值,假设为 v a l val val。我们要保证: v a l − c u r > c u r − l e f t val - cur > cur - left val−cur>cur−left 恒成立。因此 v a l > 2 ∗ c u r val > 2 *cur val>2∗cur,由于 c u r cur cur 最大为 1 0 9 10^9 109,因此 v a l = 2 ∗ 1 0 9 val = 2 * 10^9 val=2∗109
- 同理,当处于边界 2 2 2 的时候, v a l = − 2 ∗ 1 0 9 val = -2 * 10^9 val=−2∗109
其实这里还有一种更简单的做法(暴力两次遍历。。),就是直接找到每个居民左侧和右侧垃圾桶的位置,记录和当前居民的位置的差值和垃圾桶的位置,最后在比较一下判断那个更近就行了。但是这里有一个比较巧妙地优化,可以不用记录垃圾桶的位置。详见代码 1 1 1 和 2 2 2
时间复杂度 O ( N + M ) O(N+M) O(N+M)
代码1
两边扫描优化未优化空间版本
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10, INF = 2e9;
int n, m, a[N];
bool st[N];
PII l[N], r[N];
int ans[N];
int main()
{
cin >> n >> m;
for(int i = 0; i < n + m; i ++ ) cin >> a[i];
for(int i = 0; i < n + m; i ++ ) cin >> st[i];
// 从左往右扫描
int left_max = INF, left_pos = -1; // 哨兵
for(int i = 0; i < n + m; i ++ )
{
if(st[i] == 0)
{
if(left_max == INF) l[i] = {INF, m + n + 1}; // 避免爆减法爆int
else l[i] = {a[i] - left_max, left_pos};
}
else left_max = a[i], left_pos = i;
}
// 从右向左扫描
int right_min = INF, right_pos = -1;
for(int i = n + m - 1; i >= 0; i -- )
{
if(st[i] == 0)
{
if(right_min == INF) r[i] = {INF, m + n + 1};
else r[i] = {right_min - a[i], right_pos};
}
else right_min = a[i], right_pos = i;
}
for(int i = 0; i < n + m; i ++ )
{
if(st[i] == 0)
{
if(l[i].first <= r[i].first) ans[l[i].second] ++ ;
else ans[r[i].second] ++ ;
}
}
for(int i = 0; i < n + m; i ++ )
if(st[i])
cout << ans[i] << ' ';
cout << endl;
return 0;
}
代码2
两边扫描优化空间版
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10, INF = 2e9;
int n, m, a[N];
bool st[N];
int l[N], r[N];
int ans[N / 2];
int main()
{
cin >> n >> m;
for(int i = 0; i < n + m; i ++ ) cin >> a[i];
for(int i = 0; i < n + m; i ++ ) cin >> st[i];
// 从左往右扫描
int left_max = INF; // 哨兵
for(int i = 0; i < n + m; i ++ )
{
if(st[i] == 0)
{
if(left_max == INF) l[i] = INF; // 避免爆减法爆int
else l[i] = a[i] - left_max;
}
else left_max = a[i];
}
// 从右向左扫描
int right_min = INF;
for(int i = n + m - 1; i >= 0; i -- )
{
if(st[i] == 0)
{
if(right_min == INF) r[i] = INF;
else r[i] = right_min - a[i];
}
else right_min = a[i];
}
int idx = 0;
for(int i = 0; i < n + m; i ++ )
{
if(st[i] == 0)
{
if(l[i] <= r[i]) ans[idx] ++ ;
else ans[idx + 1] ++ ;
}
else idx ++ ;
}
for(int i = 1; i <= m; i ++ ) cout << ans[i] << ' ';
cout << endl;
return 0;
}
代码3
二分
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, INF = 1e9 + 1e9;
int n, m, ans[N];
int c[N << 1], a[N], b[N];
int main()
{
cin >> n >> m;
for(int i = 0; i < n + m; i ++ ) cin >> c[i];
for(int k = 0, i = 0, j = 0; k < n + m; k ++ )
{
bool t; cin >> t;
if(t) b[ ++ j ] = c[k]; // 这里是先++是为了空位0放置哨兵
else a[i ++ ] = c[k];
}
/* 哨兵
因为存在一个居民左侧没有垃圾桶或者右侧没有垃圾桶的情况,所以在二分的时候我们要设置一个边界
但是为啥设置为 2e9 呢?
*/
b[0] = -INF, b[m + 1] = INF;
for(int i = 0; i < n; i ++ )
{
int l = 0, r = m + 1, pos = a[i];
while(l < r)
{
int mid = l + r >> 1;
if(b[mid] >= pos) r = mid;
else l = mid + 1;
}
// 由于 r/l 是 i 右侧最近的,那么 r-1 的位置必然是 i 左侧最近的
if((LL)pos - b[r - 1]<= (LL)b[r] - pos) // 这里要留意是否会溢出
ans[r - 1] ++ ;
else ans[r] ++ ;
}
for(int i = 1; i <= m; i ++ ) cout << ans[i] << ' ';
cout << endl;
return 0;
}
代码4
双指针
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200010, INF = 2e9;
int n, m, c[N];
int a[N >> 1], b[N >> 1];
int ans[N >> 1];
int main()
{
cin >> n >> m;
int s = n + m;
for(int i = 0; i < s; i ++ ) cin >> c[i];
for(int k = 0, i = 0, j = 0; k < s; k ++ )
{
bool t; cin >> t;
if(t) /*rubbsh bin*/ b[ ++ j] = c[k];
else a[i ++ ] = c[k];
}
b[m + 1] = INF, b[0] = -INF;
for(int i = 0, j = 0; i < n; i ++ )
{
// 找到左侧最近的垃圾桶
while(b[j] <= a[i]) j ++ ; // 这里不需要判断 j 是否越界,因为 j 在 m+1 的位置一定会停下来,一定不会在 0 的位置停留
if((LL)a[i] - b[j - 1] <= (LL)b[j] - a[i]) ans[j - 1] ++ ;
else ans[j] ++ ;
}
for(int i = 1; i <= m; i ++ ) cout << ans[i] << ' ';
cout << endl;
return 0;
}