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}
c1…cN。
输出格式
输出写完综述后 Bessie 可以达到的最大 h指数。
数据范围
1
≤
N
≤
1
0
5
1\le N\le 10^5
1≤N≤105,
0
≤
c
i
≤
1
0
5
0\le c_{i}\le 10^5
0≤ci≤105,
0
≤
L
≤
1
0
5
0\le L\le 10^5
0≤L≤105
输入样例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。
题目解析
- h指数等于,至少有h篇文章被引用次数不少于h,的最大的一个数h
即所有文章里面,被引用次数大于等于h的论文至少有h篇 - 如果把论文引用次数放到一个集合里的话,大于等于h的数至少有h个,在所有满足要求的h当中,取一个最大的,就是h指数
- 例
(1,100,2,3),则h指数为2,因为里面有两个数大于等于2
(1,100,3,3) 则h指数将会是3,因为有三篇大于等于3
- 新写的综述,还没有被引用过,新写的综述的引用次数是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
假设先不考虑综述,h要满足条件的话,相当于是要找到,h个数大于等于h,由于已经从大到小排完序了,我们只需要看前h个数,是不是都大于等于h就可以了
如果可以选择最多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
统计一下
- 本身就已经大于等于h的数的个数
- 等于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的数
能不能找到至少h个大于等于h的数呢
h
≤
a
n
s
h \le ans
h≤ans,肯定可以
因为答案是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,整个数组是单调递减的数组,离散的
如果能找到j的话,能不能快速判断h成不成立,判断在前h个数中,等于h-1的数的个数
可以,假设对于每个h来说已经找到j了,
- 先判断前h个数是不是都大于等于h-1,也就是判断第h个数是不是大于等于h-1
- 再判断前h个数中等于h-1的数的数量是多少个
分类讨论
首先所有的数都是大于等于h-1的
所以第h个数就有两种选择
-
第h个数大于等于h,这个数本身就已经大于等于h了,也就是说大于等于h-1的最后一个数应该在这个数的右边,因此前面所有的数都不需要+1,因此是成立的
-
第h个数等于h-1,对于每个h的话,会找到它最靠右的一个大于等于h的下标j,它是大于等于h的最后一个,所以从j+1开始,就一定是h-1了,j后面的数,就全部是h-1。h左边等于h-1的数的数量就是从j+1到h的个数,也就是h-j,只要判断这个个数是不是小于等于L就可以了
对于每一个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;
}