NH集训5:综合测试

目录

前言——

正文——

        第1题     题号检索 

输入格式

输出格式

输入/输出例子1

        1.1 题目分析

        1.2 完整代码

        第2题     游乐园

输入格式

输出格式

输入/输出例子1

样例解释

        2.1 题目分析

        2.2 完整代码

        第3题     最小删除数

输入格式

输出格式

输入/输出例子1

        3.1 题目分析

        3.2 代码剖析

        3.3 完整代码

        第4题     项目经理

输入格式

输出格式

输入/输出例子1

        4.1 题目分析

        4.2 完整代码

        第5题     中位数

输入格式

输出格式

输入/输出例子1

        5.1 题目分析

        5.2 完整代码

结语——

前言——

        本套题目总体不难,对于我的状态都能想得出来。但是成绩糟糕

正文——

        我直接放题目,然后讲。

        第1题     题号检索 

        Sio 在 OJ 搜题。但因为 OJ 已经是十几年前的产物了,所以搜索功能非常垃圾。Sio 被搞得心态爆炸,于是决定自己写一个题目检索器。

        具体来说,请你实现一个题目检索器,完成如下功能:

        1.A x:加入一道题 x 或判断其已存在。

        2.D x :删除一道题 x 或判断其不存在。

        3.Q x:查询一道题 x 是否存在。

        其中,每道题都由一个独一无二的题号来代表,用一个不超过六位的数字表示(可能含有前导零)。两道题目被认为相同,当且仅当其题号是相同的。

        对于 100% 的数据,保证 q≤10^6,x 均为一个不超过5位的数字(可能含有前导零)

输入格式

        第一行一个数字 q,表示操作数量。

        接下来 q 行,每行代表一个操作,格式如上所述。

输出格式

        对于 q个操作,分别输出一个字符串,表示操作执行信息。具体地,对于三种操作:

        1.A 操作:若加入的题目已然存在,输出 NO,否则输出 YES。

        2.D 操作:若加入的题目本不存在,输出 NO,否则输出 YES。

        3.Q 操作:若查询的题目存在,输出 YES,否则输出 NO。

输入/输出例子1

        输入:

6
A 1000
A 1001
A 1000
Q 1001
D 1001
Q 1001

        输出:

YES
YES
NO
YES
YES
NO

        1.1 题目分析

                这道题目很显然要考虑存储的数据结构和查询删除的方式,然后可能要考虑前导零的问题。

                对于存储的结构,我考试的时候“杀鸡用牛刀”,写了个set,但是因为每个编号至多就6位,所以实际上采用数字进行标记即可。

                这里的数据对时间要求比较严格,读入前面的操作符的时候不能直接用cin,不然会TLE,采用一个简单的快读可以解决。前导零其实不用管,因为读入int的时候自动就去除了。

        1.2 完整代码

                代码不作解释了,及其简单。

#include<bits/stdc++.h>
using namespace std;
int n;
bool vis[1000100];
int main(){
	scanf("%d",&n);
	char op;
	int x;
	for(int i=1;i<=n;++i){
		op=getchar();
        while(op!='A'&&op!='D'&&op!='Q')
             op=getchar();
		scanf("%d",&x);
		if(op=='A'){
			if(vis[x])
				printf("NO\n");
			else{
				vis[x]=1;
				printf("YES\n");
			}
		}
		else if(op=='D'){
			if(vis[x]==0)
				printf("NO\n");
			else{
				vis[x]=0;
				printf("YES\n");
			}
		}
		else{
			if(vis[x])
				printf("YES\n");
			else
				printf("NO\n");
		}
	}
	return 0;
}

        第2题     游乐园

        市中心新开了一家游乐园!现在游乐园的负责人准备给门票定价。已知

        有 N(1≤N≤10^5)个顾客可能会购买。每个顾客最多愿意支付 ci元(1≤ci≤10^6)。每个入园的顾客均需要购买门票。如果门票费用大于顾客愿意支付的最高金额,那么这个顾客就不会入园。为了赚尽可能多的钱,负责人向你寻求帮助。请你帮他求出能赚到的钱的数量的最大值,以及此时门票的价格。

        测试点 2-4 满足 ci≤1,000。

        测试点 5-8 满足 N≤5,000。

        测试点 9-12 没有额外限制

输入格式

        输入的第一行包含 N。

        第二行包含 N 个整数 c1,c2,…,cn,为每个顾客最多愿意支付的金额。

输出格式

        输出能赚到的钱的数量的最大值以及此时门票的价格。如果有多个解,输出门票最小的解 

输入/输出例子1

        输入:

4
1 6 4 6

        输出:

12 4 

样例解释

        如果门票价格为4元,那么有 3 个顾客将会入园,从而赚取 3*4=12元

        2.1 题目分析

                这题乍一看似乎是个二分答案,每次尝试可能的门票价格然后O(n)计算,但是显然,这样暴力的代码已经达到了(10^5*log10^6)=1993160的大小,显然不是最优的。

                而且实际上如果你打了一下二分答案的代码,会发现判断极其的麻烦。因为问题本身单调性并不明显,而是答案大小的单调性。

                这里放一下当时二分答案的代码,暂供娱乐。

int judge(int x){
	int res=0;
	for(int i=1;i<=n;++i)
		res+=(c[i]>=x);
	return res;
}
void Binary_Answer(int l,int r){
	int mid=0;
	while(l<=r){
		mid=l+(r-l)/2;
		if(mid*judge(mid)>ans){
			l=mid;
			ans=mid*judge(mid);
			mon=mid;
		}
		else
			r=mid-1;
	}
}

                扯远了,再回到这题本身来。

                如果你仔细读题了,那么你会很轻松得到一个公式表达赚钱的数量:

                赚得的钱=门票价*大于等于门票价的所有游客数量

                有点抽象,那么我们完全用数学语言表达是这样(后面也如此简化)

                赚得钱数为M,门票价为P,游客数量为N

                M=P*N

                显然,这个式子乍一看是句废话,因为在数据随机给出的情况下这个式子的用处完全无法实现,当然,如果我们将所有数据从小到大排序,这个式子就可以被分解成小部分被解决了。

                我们样例排序之后是1 4 6 6,显然对于要求的M我们是需要打擂台的,现在也可以迅速求出大于一个数的个数了。现在的问题就是,我们如何确定P和N?

                P、N的确定

                如果我们顺序跑一遍,到i的位置的时候,我们的P可以设置为当前的a[i],N自然就是(n-i+1)。现在怎么证明P的确定是正确的呢?

                如果到i的位置,不设为a[i],那就意味着我们可以设为一个更小的P来换的更多的人,或者一个更大的P对应更大的M

                显然第一种可能是不成立的,a[i-1]~a[i]这一段设立P是无意义的,因为这样N并不能增加,如果设置到<=a[i-1]那么这个问题实际上就向前推到了i-1的阶段,还是符合我们的设定

                对于第二种情况,P的设立只能在a[i]~a[i+1]之间,这之间的N变成了N-1,因为不能推进到i+1的状态,所以显然P的最大值是a[i+1]-1

                但是这个时候我们发现,如果出现(a[i+1]-1)*(N-1)>a[i]*N的情况,那么在i+1的状态下我们肯定能的a[i+1]*(N-1)>a[i]*N,显然有(a[i+1]-1)*(N-1)<a[i+1]*(N-1)。

                证明结束,贪心思路可行。

                那么接下来就是最简单的排序+顺序计算了,我同样放代码。

        2.2 完整代码

//代码中,M对应ans,P对应mon,N就是n-i+1
#include<bits/stdc++.h>
#define INF 0x7fffff
using namespace std;
typedef long long ll;
const int N=1e5+10;
ll n;
ll c[N],upp=0,low=INF,ans=0,mon=0;
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i)
		scanf("%lld",&c[i]);
	sort(c+1,c+1+n);
	for(int i=1;i<=n;++i){
		ll x=c[i];
		if(x*(n-i+1)>ans){
			ans=x*(n-i+1);
			mon=x;
		}
	}
	printf("%lld %lld\n",ans,mon);
	return 0;
}

        第3题     最小删除数

        给一个含有n个点m条边的无向图,图有可能存在环,节点编号分别为1,2,3...n,现在删除一些边,使节点编号小于等于与k的都不在环上,问最少删除多少条边。

        1<=k<=n,m<=1e6

输入格式

        第一行三个数n,m,k

        接下来m行,每行两个数x,y,表示x到y有一条边

输出格式

        一个数,表示最少需要删除多少边

输入/输出例子1

        输入:

11 13 5
1 2
1 3
1 5
3 5
2 8
4 11
7 11
6 10
6 9
2 3
8 9
5 9
9 10

        输出:  

3

        3.1 题目分析

                这个题目就比较有意思了,我们先把样例模拟出来。如图

                这个图一出来实际上就很明显了,最暴力的算法就是DFS,查找有多少个包含<=k顶点的环,有几个环就要删几条边。

                但是这个时间复杂度怎么着在1e6的情况下也超时了,很显然这种暴力算法不是正解……

                照着这个思路,我一路走到黑,甚至想到拓扑,LCA判环一系列奇葩操作。

                但实际上,这题是一道稍微有点技巧的模拟题。

                对于一个环,实际上删哪条边都不影响将它变为非环形结构。所以我们被动找环的话时间就上去了,如果能在加边的过程中就判断出<=k的顶点是否在环上,我们就可以不加入这条含有<=k顶点的边,达到了去除环形的相同效果。

                同时,如果能在加边的过程中就判断,时间复杂度也变成了O(m)级别。

                现在问题来了:

                如何判断加入一条边后是否形成环?

                这个知识实际上我们早就知道——并查集。

                对于一棵树,显然每个节点都只有唯一的父节点,不存在两个节点相连又不是父子的关系。

                但是图就不一样了,因为加入的边使两个点相连,所以如果此时这两个点拥有相同的父亲,那么就意味着形成了环。

                于是,判断方法出来了。

                接下来就是一些简单的注意事项了

                为了能够每次判断的都是<=k的节点,对于一条两端顶点都是>k的边我们可以直接加入图中为后面的判断做准备。

                对于有顶点是<k的边,我们就按照上述方法判断,然后选择加不加入。

                这样,这题就结束了。

        3.2 代码剖析

                我们按照刚刚说的部分分别看一下。

                首先是第一部分,加入两个点都是>k的边

	for(int i=1;i<=m;++i){
		scanf("%d%d",&x[i],&y[i]);
		if(x[i]>k&&y[i]>k)
			fa[getr(x[i])]=getr(y[i]);//注意都要取根,不然后面无法判断
	}

                然后第二部分,就是简单的判断

	for(int i=1;i<=m;++i){
		if(x[i]>k&&y[i]>k)//之前已经加过了
			continue;
		int rx=getr(x[i]);//分别去根节点
		int ry=getr(y[i]);
		if(rx==ry)//成环,不能加入
			++ans;
		else
			fa[rx]=ry;//加边
	}

                最后就是并查集的部分了,我直接放完整代码。

        3.3 完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e6+100;
int n,m,k;
int x[N],y[N];
int fa[N];
int getr(int x){
	int r=x;
	while(fa[r]!=r)
		r=fa[r];
	int t=0;
	while(fa[x]!=r){
		t=x;
		x=fa[x];
		fa[t]=r;
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=n;++i)
		fa[i]=i;
	for(int i=1;i<=m;++i){
		scanf("%d%d",&x[i],&y[i]);
		if(x[i]>k&&y[i]>k)
			fa[getr(x[i])]=getr(y[i]);
	}
	int ans=0;
	for(int i=1;i<=m;++i){
		if(x[i]>k&&y[i]>k)//之前已经加过了
			continue;
		int rx=getr(x[i]);//分别去根节点
		int ry=getr(y[i]);
		if(rx==ry)//成环,不能加入
			++ans;
		else
			fa[rx]=ry;//加边
	}
	printf("%d\n",ans);
	return 0;
}

        第4题     项目经理

        某公司要招聘一个能力强的项目经理,去完成一些项目,一共有n个项目,每个项目i有两个属性,第一个属性a(i)表示项目经理的声望应该至少是a(i)的时候才有资格去做这一个项目,第二个属性b(i),表示项目经理做完这个项目后声望值会增加b(i),注意b(i)有可能为负数,另外,做完每一个项目后项目经理的声望值不能小于0,否则会被老板不信任。项目经理的初始声望为s,问他最多能够完成几个项目,选择尽可能多的项目,并且要求完成每个项目后声望值都大于等于0。

输入格式

        第一行两个整数n,s (1<=n<=100,1<=s<=30000)

        接下来n行,每行两个整数a(i),b(i) (1<=a(i)<=30000, -300<=b(i)<=300)

输出格式

        输出个数

输入/输出例子1

        输入:

3 4
4 6
10 -2
8 -1

        输出:

3

        4.1 题目分析

                这个题第一眼望去是贪心,然后你会发现的的确确是贪心,然后你开始研究贪心思路,然后最后发现这题评为蓝题实在是名副其实的难,因为这题远不止贪心那么简单。

                当然,我们还是先顺着贪心的思路来,先想明白贪心的部分怎么做。

                首先对于bi是正数的情况,显然,这部分可以直接加进s的部分,当然也要满足ai的要求。

                第一部分贪心出来了,对于bi>=0的情况,我们的依据是:x.ai<y.ai,然后循环判断满足ai<=s的情况,就直接更新s。

                接下来就是最难的部分,对于bi<0的情况。

                我们肯定还是要找个排序的方式然后想办法循环加入。

                这是个非常难证明的贪心,但是规律是可以靠硬枚举样例得出来的
                如果bi<0,那么排序的依据是x.ai+x.bi>y.ai+y.bi

                因为这个依据不是靠人力所能想出来的,所以一下我们均建立在已知的基础上进行证明,最后再来讲这样的反人类题目怎么办。

                样例中,我们的选择必须是先选10 -2,再选8 -1。现在我们放宽条件。

                假设排序后的序列是A1,A2……An,那么如果出现A2 A1……An满足条件,A1,A2……An一定满足条件,一般状况下A1,A2……An满足条件。

                那么,如果A2先选就意味着此时有:

                s>=A2.ai

                因为再选了A1,所以

                s+A2.bi(bi<0)>=A1.ai

                因为最终s还要>=0,所以

                s+A1.bi+A2.bi>=0

                同理对于第二种序列,可以得需证明在满足

                s>=A2.ai  s+A2.bi(bi<0)>=A1.ai s+A1.bi+A2.bi>=0的情况下有

                1 s>=A1.ai  2 s+A1.bi(bi<0)>=A2.ai 3 s+A1.bi+A2.bi>=0

                3条件不需要证明,两组式子相同

                1条件因为有s+A2.bi>=A1.ai,A2.bi<0,所以得s>=A1.ai-A2.bi,s>=A1.ai

                2条件移项得:s>=A2.ai-A1.bi 因为有个最基本的式子 A1.ai+A1.bi>A2.ai+A2.bi,所以再次移项,又可以得A1.ai-A2.bi>A2.ai-A1.bi,又因为1中证明了s>=A1.a,则s+A1.bi>=A2.ai

                贪心证明完毕

                那么我们的第二部分也就有了排序的依据,然后你是不是就想着再跑一遍循环??

                注意,我们这里的排序依据只能保证A1.ai+A1.bi>A2.ai+A2.bi,但是并没有证明一定存在s+A1.bi+A2.bi……+Ax.bi>Ax+1.ai

                也就是说,我们不能保证选择的路上是一帆风顺的。

                你肯定以为碰到没法选的就弹掉,那你就草率了。

                看个反例:

s=6
A1 10 -8
A2 3 -2
A3 3 -2

                如果顺着跑只能取到1个,但是实际上取A2,A3更优,有两个。

                怎么办呢???

                是不是对于每个Ai我们可以选或者不选,然后最终从上一个状态判断现在怎么转移到下一个状态?

                好的,我说的是有点太明显了。但是这里的的确确是个非常经典的选择模型了。这里,采用01背包。

                状态设计很简单:dp[i]表示声望值为i的时候最多选择多少个项目

                转移我不想说了。(等我DP

                最后,我们理一遍思路:

                第一步,对于bi>=0的部分贪心排序+选择

                第二步,对于bi<0的部分贪心排序

                第三步,对于bi<0的部分DP的选择

                我就不细讲代码了,因为贪心也没什么可讲的。自己看注释吧。

        4.2 完整代码

#include<bits/stdc++.h>
#define INF 0x7fffff
using namespace std;
const int N=5e5+100;
int n,s,x,y,len1,len2;
int dp[N];
struct node{
	int ai;
	int bi;
}a[N],b[N];
bool cmp1(node x,node y){//bi>=0的排序
	return x.ai<y.ai;
}
bool cmp2(node x,node y){//bi<0的排序
	return x.ai+x.bi<y.ai+y.bi;
}
int main(){
	scanf("%d%d",&n,&s);
	for(int i=1;i<=n;++i){
		scanf("%d%d",&x,&y);
        //对于bi的情况分两个数组存
		if(y>=0)
			a[++len1]={x,y};
		else
			b[++len2]={x,y};
		}
	sort(a+1,a+1+len1,cmp1); 
    //正bi的贪心选择
	int ans=0;
	for(int i=1;i<=len1;++i){
		if(s<a[i].ai)//选不了的之后都没得选
			break;
		if(s>=a[i].ai){
			s+=a[i].bi;
			++ans;
		}
	}
	sort(b+1,b+1+len2,cmp2);//bi<0的排序
    //01背包选择
	for(int i=1;i<=len2;++i){
		for(int j=s;j>=b[i].bi;--j)
            //注意要满足>=ai才能选择
			if(j>=b[i].ai&&j+b[i].bi>=0)
				dp[j]=max(dp[j],dp[j+b[i].bi]+1);
	}
	printf("%d\n",ans+dp[s]);//输出
	return 0;
}

                

        第5题     中位数

        一个长度为n(1<=n<=2e5)的序列a,定义中位数为一段连续的数升序排序后中间的那个数。如果长度为偶数len,则中位数为第(len/2向下取整)个数。求序列a所有长度大于等于k(1<=k<=2e5)的连续子序列中,中位数的最大可能的取值。

        对于百分之五十的数据保证 1<=k<=n<=1000,另外百分之五十1<=k<=n<=2e5

输入格式

        第一行两个数,n,k

        接下来一行n行每行一个数,表示序列a的n个元素

输出格式

        一个数中位数最大可能的取值

输入/输出例子1

        输入:

5 3
1 
2
3
2
1

        输出:

2

        5.1 题目分析

                这题带点数学的思路在里面,因为没有数学的知识你是设计不出这个鬼算法的。

                一个序列长度为len,他的中位数为x

                如果len是奇数,那么<x的数显然有至少有(len-1)/2个,>=x的数显然至少(len-1)/2+1个

                如果len是偶数,那么<x的数显然至少有len/2-1个,>=x的数显然至少有len/2+1个

                这个似乎想也是能想出来的,但是又什么用呢?

                那就是说,如果我们记>=x的数有cnt2个,<x的数有cnt1个,显然cnt2-cnt1>=1

                是的,但是还是看不出来有什么用。

                假设我们已经知道了一个序列的中位数,那么是不是我们就能搞出这个序列的cnt2的前缀和数组和cnt1的前缀和数组?

                然后,我们现在是不是要找长度大于等于k的序列的中位数?

                假设现在长度就是k

                那么我们枚举每个可能序列的右端点,如果这个序列合法(这个长度为k的序列中位数是x),显然有(cnt2[i]-cnt2[k-1])-(cnt1[i]-cnt1[k-1])>=1

                如果长度>k怎么办?

                那就不一定是cnt2[k-1]和cnt1[k-1]了。现在,在端点i固定的情况下cnt2[i]和cnt1[i]固定(此处的cnt均是前缀和数组了),那么如果想让情况更加极端就是使(cnt2[i]-cnt2[k-x])和(cnt1[i]-cnt1[k-x])两个序列的值尽可能大,也就需要cnt2[k-x]和cnt1[k-x]尽可能小。

                如果你还不明白,我们变个形。

                (cnt2[i]-cnt2[k-x])-(cnt1[i]-cnt1[k-x])>=1等价于cnt2[i]-cnt1[i]+cnt1[k-x]+cnt2[k-x]>=1

                如果最小的cnt1[k-x]+cnt2[k-x]都满足条件,那么显然更大的cnt1[k-x]+cnt2[k-x]也能满足条件了。

                而且,我们现在就可以将cnt2和cnt1合并成他们的差值了,这样的效果也是等价的。你可以自己再变形式子。

                那么现在,对于已知中位数的情况,我们已经可以判定是否存在合法的序列,这个复杂的是O(n)。

                但是现在怎么“已知中位数”?

                肯定不能枚举了,我们现在的算法极限是nlogn,所以找中位数就需要更快。

                二分啊!!

                这题现在看是不是标准的二分答案?我们刚刚是不是已经讨论了如何实现check函数?!

                那么,接下来看代码。

        5.2 完整代码

#include<bits/stdc++.h>
#define INF 0x7fffff
using namespace std;
const int N=2e5+10;
int n,k;
int a[N],sum[N],minn[N];
bool check(int x){
	for(int i=1;i<=n;++i){
        //这里为了方便,我们不统计两个数组
        //因为最终需要cnt2-cnt1,所以cnt1对应的元素就相应设为-1
		if(a[i]>=x) 
			sum[i]=sum[i-1]+1;//求前缀差值
		else
			sum[i]=sum[i-1]-1;
        //求前缀最小值,保证后面的>=k查询
		minn[i]=min(minn[i-1],sum[i]);
	}
	for(int i=k;i<=n;++i)//枚举所有右端点
		if(sum[i]-minn[i-k]>0)
			return 1;
	return 0;
}
void Binary_Ans(int l,int r){
    //二分答案板子
	while(l+1<r){
		int mid=l+(r-l)/2;
		if(check(mid))
			l=mid;
		else
			r=mid;
	}
	printf("%d\n",l);
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	Binary_Ans(0,N);
	return 0;
}

结语——

        已经是23:35,本文终于全部结束。

        总的来说,这套题的思维难度实际上并不大(第四题的DP和第五题的数学变化确实是变态),但是后面几题难度还是不低。T4蓝题。

        对于已有的知识,往简单的方面想,千万不要剑走偏锋,杀鸡用牛刀。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值