【AcWing】蓝桥杯集训每日一题Day4|双指针|二分|3745.牛的学术圈(C++)

3745.牛的学术圈 I

3745. 牛的学术圈 I - AcWing题库
难度:简单
时/空限制:1s / 64MB
总通过数:7419
总尝试数:27093
来源:
USACO 2021 US Open Bronze
算法标签 双指针二分

题目内容

由于对计算机科学的热爱,以及有朝一日成为「Bessie 博士」的诱惑,奶牛Bessie开始攻读计算机科学博士学位。
经过一段时间的学术研究,她已经发表了N篇论文,并且她的第i篇论文得到了来自其他研究文献的 c i c_{i} ci次引用。
Bessie 听说学术成就可以用 h指数来衡量。
h指数等于使得研究员有至少h篇引用次数不少于h的论文的最大整数h。
例如,如果一名研究员有4篇论文,引用次数分别为(1,100,2,3),则h指数为2,然而若引用次数为 (1,100,3,3) 则h指数将会是3。
为了提升她的h指数,Bessie 计划写一篇综述,并引用一些她曾经写过的论文。
由于页数限制,她至多可以在这篇综述中引用L篇论文,并且她只能引用每篇她的论文至多一次
请帮助 Bessie 求出在写完这篇综述后她可以达到的最大h指数。
注意 Bessie 的导师可能会告知她纯粹为了提升h指数而写综述存在违反学术道德的嫌疑;我们不建议其他学者模仿 Bessie 的行为。

输入格式

输入的第一行包含 N 和 L。
第二行包含 N个空格分隔的整数 c 1 … c N c_{1}\dots c_{N} c1cN

输出格式

输出写完综述后 Bessie 可以达到的最大 h指数。

数据范围

1 ≤ N ≤ 1 0 5 1\le N\le 10^5 1N105,
0 ≤ c i ≤ 1 0 5 0\le c_{i}\le 10^5 0ci105,
0 ≤ L ≤ 1 0 5 0\le L\le 10^5 0L105

输入样例1:
4 0
1 100 2 3
输出样例1:
2
样例1解释

Bessie 不能引用任何她曾经写过的论文。上文中提到,(1,100,2,3) 的h指数为2。

输入样例2:
4 1
1 100 2 3
输出样例2:
3

如果 Bessie 引用她的第三篇论文,引用数会变为 (1,100,3,3)。上文中提到,这一引用数的 h 指数为 3。

题目解析
  1. h指数等于,至少有h篇文章被引用次数不少于h,的最大的一个数h
    即所有文章里面,被引用次数大于等于h的论文至少有h篇
  2. 如果把论文引用次数放到一个集合里的话,大于等于h的数至少有h个,在所有满足要求的h当中,取一个最大的,就是h指数

  • (1,100,2,3),则h指数为2,因为里面有两个数大于等于2
    (1,100,3,3) 则h指数将会是3,因为有三篇大于等于3
  1. 新写的综述,还没有被引用过,新写的综述的引用次数是0,所以统计h指数的时候不用统计新写的综述

N表示N篇文章的被引用次数,L表示写的综述最多可以引用的文章数量
N最大是 1 0 5 10^5 105,所以算法复杂度需要控制在 O ( log ⁡ n ) O(\log n) O(logn)以内

为了方便,可以先将文章的引用次数从大到小排个序
可以枚举一下h指数,找到一个最大的满足要求的h指数

枚举完h之后,如何判断这个h指数能不能达到呢
如何判断写完综述之后,能不能找到至少h个数,全部大于等于h

![[Pasted image 20240313200016.png]]

假设先不考虑综述,h要满足条件的话,相当于是要找到,h个数大于等于h,由于已经从大到小排完序了,我们只需要看前h个数,是不是都大于等于h就可以了

![[Pasted image 20240313200224.png]]

如果可以选择最多L个数,全部+1的话,怎么判断h指数
多了这一个操作的话,要看h成不成立的话,并不一定要求1h全部大于等于h,因为有些数可以+1,所以只需要1h里面,前面若干个大于等于h,因为最多可以让L个数+1,所以最多可以有L个数等于h-1,不用达到h

判断这样一个事情:
给定h之后,在前h个数中,是不是所有数都是大于等于h-1的,且其中最多有L个数等于h-1,因为最多只能选择L个数全部+1

因为已经从大到小排完序了
所以判断所有数是不是大于等于h-1,判断一下 c h c_{h} ch是不大于等于h-1就可以了
是倒序排的,所以第h个数就是前h个数中最小的那个

判断最多L个数等于h-1

如果暴力的话,枚举一下就可以,用两层循环,第一层循环枚举h,第二层循环枚举前h个数,看一下有多少个数等于h-1
暴力算法的时间复杂度就是 O ( n 2 ) O(n^2) O(n2)

二分

h是可以二分的,二分完h之后,按照暴力方法从左往右枚举一遍,判断成不成立
可以跳到 O ( n log ⁡ n ) O(n\log n) O(nlogn)的时间复杂度

断裂式二分
给一个原数组,如果最多只能挑L个数+1后,能否找到h个数大于等于h
统计一下

  1. 本身就已经大于等于h的数的个数
  2. 等于h-1的数的个数
    加一的话只会加到h-1上,如果本身已经大于等于h的话,加不加一无所谓
    也不会加到小于h-1的数的上面,加上了一个一以后也不可能大于等于h,因此能够有影响的就只有在等于h-1的基础上去加

可以先枚举一遍,统计一下有多少个数大于等于h,有多少个数等于h-1

接下来判断一下能不能给最多L个数加一,使得大于等于h的数的数量至少有h个
如果大于等于h的数的数量是a,等于h-1的数的数量是b的话,要加1的话肯定加在b上

能把多少个b变成h呢

min{b, L}+a
最多只有b个,不能超过b;最多只能加L个,取一个min,再加个a,就是最终可以得到的大于等于h的数量

发现h可以二分
答案是从1~n中去选,存在一个答案,大于答案的,肯定是无解的
对于任意一个小于等于答案的,显然一定有解,
如果ans是答案的话,一定可以找到至少ans个大于等于ans的数
![[Pasted image 20240313202518.png]]

能不能找到至少h个大于等于h的数呢
h ≤ a n s h \le ans hans,肯定可以
因为答案是ans,可以至少找到ans个大于等于ans的数
这些个数看作一个集合,每一个都大于等于ans,并且集合大小是大于等于ans的
现在找了一个比ans小的数h
这个集合是大于等于ans,就可能大于等于h
所以只要ans成立,小于ans的就一定是有解的

它是有二段性的,因此可以通过二分把中点找出来

代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, L;
int w[N];  //用w来表示被引用次数

bool check (int mid)
{
	int a = 0, b = 0;
	for (int i = 1; i <= n; i ++)
	{
		if (w[i] >= mid) 
			a++;
		else if (w[i] == mid - 1)
			b++;
	}
	return a + min(b, L) >= mid;
}

int main()
{
	scanf("%d%d", &n, &L); //读入一下论文数量和可以被引用的数量
	for (int i = 1; i <= n; i ++)
		scanf("%d", &w[i]);  //读入每篇文章被引用的次数

	//二分一下答案
	int l = 0, r = n;
	while (l < r)
	{
		int mid = l + r + 1 >> 1; //每次求一下中点
		if (check(mid)) //判断一下二分成不成立,成立的话,答案大于等于mid
			l = mid;
		else
			r = mid - 1;
	}
	printf("%d\n", r); //输出一下二分的中界点就可以了
	return 0;
}

双指针

可以把后一步优化到 O ( n ) O(n) O(n)
找一下单调性
对于每一个h,要找到前h个数中,等于h-1的数的数量
枚举h

怎么找单调性

对于每个h,找到大于等于h的最后一个数的下标,设为j
对于横坐标是下标j,纵坐标是数组里面的每一个值 c i c_{i} ci,整个数组是单调递减的数组,离散的

![[Pasted image 20240313200536.png]]

如果能找到j的话,能不能快速判断h成不成立,判断在前h个数中,等于h-1的数的个数

可以,假设对于每个h来说已经找到j了,

  1. 先判断前h个数是不是都大于等于h-1,也就是判断第h个数是不是大于等于h-1
  2. 再判断前h个数中等于h-1的数的数量是多少个
分类讨论

首先所有的数都是大于等于h-1的
所以第h个数就有两种选择

  1. 第h个数大于等于h,这个数本身就已经大于等于h了,也就是说大于等于h-1的最后一个数应该在这个数的右边,因此前面所有的数都不需要+1,因此是成立的
    ![[Pasted image 20240313201554.png]]

  2. 第h个数等于h-1,对于每个h的话,会找到它最靠右的一个大于等于h的下标j,它是大于等于h的最后一个,所以从j+1开始,就一定是h-1了,j后面的数,就全部是h-1。h左边等于h-1的数的数量就是从j+1到h的个数,也就是h-j,只要判断这个个数是不是小于等于L就可以了
    ![[Pasted image 20240313201344.png]]

对于每一个h来说,在一个单调递减的数组里,找到大于等于h的最后一个数,是可以使用双指针算法的
从小到大枚举i,由于整个数组是单调递减的,随着i变大,大于等于i的最后一个数,j也是单调递减的,
比如i=0的时候,大于等于i的最后一个位置也就是j=n
下标越来越小,
因此j关于i是单调的,所以可以用双指针算法

双指针

当且仅当一个下标关于另一个下标单调,就可以用

总结

首先,给定一个h的话,如何判断h是不是成立的,就是判断前h个数,是不是大于h-1,且其中最多有L个数等于h-1,这样的话,把这L个数全部+1,才可以全部大于等于h
为了快速判断这样的事情,必须预处理一下,对于每个数h来说,找到大于等于h的最后一个数的下标,记为f(h),就可以快速判断这个条件了

判断前h个数是不是都大于等于h-1,也就是判断第h个数是不是大于等于h-1
如果第h个数,已经大于等于h了,也就是一个数都不用加,成立
如果小于h的话,由于全部大于等于h-1,所以肯定等于h-1,f(h)存的就是左边第一个大于等于h的数,f(h)右边第一个数就等于h-1,因此总共等于h’-1的数量就是h-f(h)

如何快速预处理我们每个数大于等于每个数的最后一个位置

如何快速预处理我们每个数大于等于每个数的最后一个位置
原数组是单调递减的,当枚举的h在递增的时候,大于等于它的最后一个数会单调递减,所以可以用双指针算法

代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, L;
int w[N];  //用w来表示被引用次数

int main()
{
	scanf("%d%d", &n, &L); //读入一下论文数量和可以被引用的数量
	for (int i = 1; i <= n; i ++)
		scanf("%d", &w[i]);  //读入每篇文章被引用的次数
	//从大到小排个序
	sort (w + 1, w + n + 1, greater<int>());

	int res = 0;  //定义一下答案

	//从前往后一次枚举每一个h
	for (int i = 1, j = n; i <= n; i ++) //维护一个双指针,最多从n开始
	{
		//当没有走完,并且w[j]小于i的话,j--
		while (j && w[j] < i)
			j --;
		if (w[i] >= i - 1 && i - j <= L)  //判断一下第i个数是不是大于等于i-1
			res = i;
	}
	printf("%d\n", res);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值