9.16 p.m.小结

T1:问题 A: NOIP題海战

题目描述

某信息学奥赛教练经验丰富,他的内部题库有 m 道题。他有 n 个学生,第 i 个学生完成了p[i]道题。由于马上要进行 NOIP 的考试,该教练便举行了 k 场比赛和训练,每场比赛式训练都会有一些他的学生参加,但是如何选题令他非常烦恼。对于每场比赛,他要保证所出的题没有任何一道已有任何一个参赛学生做过;而对于每场训练,他要保证所出的所有题都被每一个参赛学生做过。

输入

第 1 行 2 个正整数 n 和 m,表示学生数和题库中的题目总量。
第 2~n+1 行,先是 1 个正整数 p,然后 p 个整数表示第 i 个学生的做题记录(可能重复做同一道题)。
第 n+2 行,1 个正整数 k,表示要举行的比赛和训练总数(可能有学生重复报名)。
接下来的 k 行,每行的第 1 个整数 type 表示是训练或者比赛(1 为训练,0 为比赛)。第 2个数 q 表示参赛学生数,然后 q 个正整数表示参赛学生编号。每一行中的两个数之间有一个空格。

输出

共 k 行,每行表示本次训练或比赛可选的题目(由小到大排序,中间用一个空格隔开,如果没有输出一个空行)。

样例输入

5 10

2 3 7

1 3

2 4 7

3 3 6 10

7 1 2 3 4 7 8 9

6

0 3 3 4 5

0 3 1 3 4

1 2 1 3

0 1 5

1 1 2

1 2 3 5

样例输出

5

1 2 5 8 9

7

5 6 10

3

4 7

题解

C++去重哪家强,STL找set。这道题中“可能重复做同一道题”,“可能有学生重复报名”摆明了要用到去重。然后根据正难则反的思想(这里不是为了避免重复,而是提高效率),它要测试我就找比赛,它找比赛我做测试,然后用VIS数组存一下访问过的,最后剩下的就是本次比赛(或测试)的答案。Set用法很简单,这里就不给出那种代码了。现在来看没有set怎么办,用桶!桶也是能去重的重要手段,特别是在空间能承受的范围内。因此思路很明确:用桶搞定学生的做题记录,以及非做题记录,再在每次询问中用正难则反的思想,很快的处理出答案。

参考代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,k,vis[50001][101][2];
int pds[200],s[50010],tot=0;
int rs[50010];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		memset(s,0,sizeof(s));
		int p;scanf("%d",&p);
		while(p--)
		{
			int x;scanf("%d",&x);
			if(s[x]) continue;
			s[x]=1;
			vis[++vis[0][i][1]][i][1]=x;
		}
		for(int j=1;j<=m;j++)
		{
			if(!s[j])
			vis[++vis[0][i][0]][i][0]=j;
		}
	}
	scanf("%d",&k);
	while(k--)
	{
		memset(rs,0,sizeof(rs));
		int pd,num;tot=0;
		scanf("%d%d",&pd,&num);
		for(int i=1;i<=num;i++)
		{
			int x;scanf("%d",&x);
			pds[i]=x;
		}
		if(pd==0)
		{
			for(int i=1;i<=num;i++)
			{
				for(int j=1;j<=vis[0][pds[i]][1];j++)
				{
					rs[vis[j][pds[i]][1]]=1;
				}
			}
		}
		else
		{
			for(int i=1;i<=num;i++)
			{
				for(int j=1;j<=vis[0][pds[i]][0];j++)
				{
					rs[vis[j][pds[i]][0]]=1;
				}
			}
		}
		for(int i=1;i<=m;i++)
		if(!rs[i]) printf("%d ",i);
		printf("\n");
	}
	return 0;
}

T2:问题 B: 有序表的最小和

题目描述

给出两个长度为n的有序表A和在A和B中各任取一个元素,可以得到n^2个和,求 这些和中最小的n个。

输入

第一行包含一个整数n(n<400000);
第二行与第三行分别有n个整数,分别代表有序表A和B。整数之间由一个空格隔开, 大小在长整型范围内,保证有序表的数据单调递增。

输出

S输出共n行,每行一个整数.第i行为第i小的和。数据保证在长整型范围内。

样例输入

3

1 2 5

2 4 7

样例输出

3

4

5

题解

这道题考堆。

如果无法立刻看出来,不妨举个例子。

假设A中的数为:1 2 3,B中的数为2,3,4(已排序)

A   1   2   3

B

1    2   3   4

2    3   4   5

3    4   5   6

由此可见,每一行都严格递增,就是“单调队列”!

因此每次答案都是在每一行队首(不一定是第一个)中的最小值取到,即会维护一个大小为n的小根堆,就能nlog2n完成本题。

参考代码

#include<cstdio>
#include<algorithm>
#define LL long long 
#include<queue>
using namespace std;
struct node
{
	int num1,num2;
	LL val1,val2;
};
priority_queue<node>q;
bool operator < (node m,node n)
{
	return m.val1+m.val2>n.val1+n.val2;
}
int n,tot=0;
LL a[500010],b[500010];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++) scanf("%lld",&b[i]);
	sort(a+1,a+n+1);
	sort(b+1,b+n+1);
	for(int i=1;i<=n;i++)
	{
		node pt;
		pt.num1=1;pt.val1=a[1];
		pt.num2=1;pt.val2=b[i];
		q.push(pt);	
	}
	for(int i=1;i<=n;i++)
	{
		node cy=q.top();
		printf("%lld\n",cy.val1+cy.val2);
		q.pop();
		cy.num1++;
		cy.val1=a[cy.num1];
		q.push(cy);
	}
	return 0;
}

题解

  首先根据题意,相互憎恨的骑士不能坐在相邻两个位置,又因为出席的骑士数为奇数,因此就意味着如果与一个骑士不相互憎恨的人数小于等于1,那么这个骑士就会被踢掉。现在来考虑踢掉这个骑士会产生什么效果。

  首先,憎恨他的人不会因此而多一个朋友,所有剩下的骑士中该踢的还是会踢。其次,原本所有与这个骑士可以做朋友的人都会少一个朋友,即他们有可能会被踢掉。总的来说,对于一个能踢的骑士,无论怎样踢其他人都无法使得他不会被踢,所以该踢的骑士必须踢。乍一听,这是道模拟?咋可能,m的数据都10的6次方了,并且还是多组数据,因此每次O(N)扫描不现实。

  可以尝试用一个堆,记录与某个骑士做朋友的人有多少,每次取最少的,然后用邻接表的方式找到他的朋友,并且让他朋友的朋友数减一。现在再来看一看效率,首先建立一个堆是O(nlogn),然后要让朋友的朋友数在堆中得到改变,又是一个O(nlogn),一个人的极限朋友是1个,因此总效率(n*nlogn),应该是比较优的效率了。至于为什么改变朋友的朋友总数是O(nlogn),因为可能出现如下极端情况,一个人原本朋友很多,结果那些朋友都很快就被踢了,最后他也要被踢,但是pop却一直没法找到他(优先队列),就可能出现答案错误。由此我们再来想一个方法,不用堆。

  能否用一个朴素的数组来解决这道题呢?显然是可以的。首先把数组按照朋友数升序排列,然后从朋友数最少的开刀。由于一个骑士被踢了,会最多导致另一个骑士可能被踢,因此只需要记录那个骑士还剩下多少个朋友(不需要准确的编号),直到朋友数小于等于1了(后面的应该是只可能等于1 啊,但是对于前面的来说也不排除等于0的可能),就直接对这个人进行操作了。由此每一次踢人是O(n)的,找到朋友总体平均下来是O(n)的,统计答案也是O(n)的。唯一让人有些头疼的是记录朋友数……直接记录敌人数,然后总人数减去敌人数再减1不就是朋友数了吗?至于快速找到唯一的朋友,可以考虑用二维数组vis[1001][1001]来直接记录,同时用一个链表来实时更改朋友链接(即快速由一个朋友转移到另一个朋友),可以桶排序后O(n*n)来解决。就可以以整体效率O(n*n*t)来完成本题。

参考代码

题解

  如果说只有长度一个条件,这道题该如何处理?如果先按照长度升序排序,就能采用拦截导弹贪心的思想,每一次用最大满足条件的数来继承,如果一个都不满足条件就ans++。用这种方式是能够保证正确性的,因为对于这个数而言,达成的效果只可能是把一组数的最大值推到这个数,而由什么推过来却有选择,用这种贪心方式是能够让效益最大化。

参考代码

T4:问题 D: 区间和

题目描述

给定一数列,规定有两种操作,一是修改某个元素,二是求区间的连续和。

输入

输入数据第一行包含两个正整数n,m(n<=100000,m<=500000),以下是m行,
每行有三个正整数k,a,b(k=0或1, a,b<=n).k=0时表示将a处数字加上b,k=1时表示询问区间[a,b]内所有数的和。

输出

对于每个询问输出对应的答案。

样例输入

10 20

0 1 10

1 1 4

0 6 6

1 4 10

1 8 9

1 4 9

0 10 2

1 1 8

0 2 10

1 3 9

0 7 8

0 3 10

0 1 1

1 3 8

1 6 9

0 5 5

1 1 8

0 4 2

1 2 8

0 1 1

样例输出

10

6

0

6

16

6

24

14

50

41

题解

单点修改,区间查询,线段树首选。因此直接用线段树的板就可以过了(我真的不是想水题……)。

参考代码

#include<cstdio>
#define lc (x<<1)
#define rc (x<<1|1)
#define LL long long
using namespace std;
struct tree
{
	int l,r;LL sum;
}ask[1000010*4];
int n,m;
void make_tree(int x,int l,int r)
{
	ask[x].l=l;ask[x].r=r;
	if(l==r) 
	{
		ask[x].sum=0;
		return;
	}
	int mid=(l+r)/2;
	make_tree(lc,l,mid);
	make_tree(rc,mid+1,r);
}
void update(int x,int pos,LL k)
{
	if(ask[x].l==ask[x].r)
	{
		ask[x].sum+=k;
		return;
	}
	int mid=(ask[x].l+ask[x].r)/2;
	if(pos<=mid) update(lc,pos,k);
	else update(rc,pos,k);
	ask[x].sum=ask[lc].sum+ask[rc].sum;
}
LL query(int x,int l,int r)
{
	if(ask[x].l>r||ask[x].r<l) return 0;
	if(ask[x].l>=l&&ask[x].r<=r) return ask[x].sum;
	return query(lc,l,r)+query(rc,l,r);
}
int main()
{
	scanf("%d%d",&n,&m);
	make_tree(1,1,n);
	while(m--)
	{
		int pd;
		scanf("%d",&pd);
		if(pd==0)
		{
			int pos;LL k;
			scanf("%d%lld",&pos,&k);
			update(1,pos,k);
		}
		else
		{
			int l,r;
			scanf("%d%d",&l,&r);
			printf("%lld\n",query(1,l,r));
		}
	}
	return 0;
}

T5:问题 E: 堆排序(heapsort)★

题目描述

    假设n个数存放在a[1.. n]中,我们可以利用堆将它们从小到大进行排序,这种排序方法称为“堆排序”。输人两行,第1行为n,第2行为n个整数,每个数之间用1个空格隔开。输出1行,为从小到大排好序的n个数,每个数之间也用1个空格隔开。

输入

两行,第1行为n,第2行为n个整数,每个数之间用1个空格隔开。

输出

1行,为从小到大排好序的n个数,每个数之间用1个空格隔开。

样例输入

5

5 3 4 1 2

样例输出

1 2 3 4 5

题解

说来惭愧,考试时看到这题,我机智的认为绝对不可能是优先队列这么简单,因此我用的归并排序……“堆”是一种重要的数据结构,这道题的意义在于要手写堆排序(就是手写一个优先队列),以下给出push与pop的手写版。(swap为交换)

参考代码(push

void push1(int k)
{
	heap[++heap_size]=k;
	int son,pa;
	son=heap_size;
	while(son>1)
	{
		pa=son/2;
		if(heap[pa]>=heap[son]) break;
		else swap1(heap[son],heap[pa]);
		son=pa;
	}
}

参考代码(pop

void pop1(int k)//剔除数组中第k个(一般只用1)
{
	heap[k]=heap[heap_size--];
	int son,pa;
	son=k;
	while(son>1)
	{
		pa=son/2;
		if(heap[pa]>=heap[son]) break;
		else swap1(heap[son],heap[pa]);
		son=pa;
	}
	pa=k;
	while(pa*2<=heap_size)//广义的pop
	{
		son=pa*2;
		if(son<heap_size&&heap[son]<heap[son+1]) son++;
		if(heap[pa]>=heap[son]) break;
		else swap1(heap[son],heap[pa]);
		pa=son;
	}
}

题解

  首先根据题意,相互憎恨的骑士不能坐在相邻两个位置,又因为出席的骑士数为奇数,因此就意味着如果与一个骑士不相互憎恨的人数小于等于1,那么这个骑士就会被踢掉。现在来考虑踢掉这个骑士会产生什么效果。

  首先,憎恨他的人不会因此而多一个朋友,所有剩下的骑士中该踢的还是会踢。其次,原本所有与这个骑士可以做朋友的人都会少一个朋友,即他们有可能会被踢掉。总的来说,对于一个能踢的骑士,无论怎样踢其他人都无法使得他不会被踢,所以该踢的骑士必须踢。乍一听,这是道模拟?咋可能,m的数据都10的6次方了,并且还是多组数据,因此每次O(N)扫描不现实。

  可以尝试用一个堆,记录与某个骑士做朋友的人有多少,每次取最少的,然后用邻接表的方式找到他的朋友,并且让他朋友的朋友数减一。现在再来看一看效率,首先建立一个堆是O(nlogn),然后要让朋友的朋友数在堆中得到改变,又是一个O(nlogn),一个人的极限朋友是1个,因此总效率(n*nlogn),应该是比较优的效率了。至于为什么改变朋友的朋友总数是O(nlogn),因为可能出现如下极端情况,一个人原本朋友很多,结果那些朋友都很快就被踢了,最后他也要被踢,但是pop却一直没法找到他(优先队列),就可能出现答案错误。由此我们再来想一个方法,不用堆。

  能否用一个朴素的数组来解决这道题呢?显然是可以的。首先把数组按照朋友数升序排列,然后从朋友数最少的开刀。由于一个骑士被踢了,会最多导致另一个骑士可能被踢,因此只需要记录那个骑士还剩下多少个朋友(不需要准确的编号),直到朋友数小于等于1了(后面的应该是只可能等于1 啊,但是对于前面的来说也不排除等于0的可能),就直接对这个人进行操作了。由此每一次踢人是O(n)的,找到朋友总体平均下来是O(n)的,统计答案也是O(n)的。唯一让人有些头疼的是记录朋友数……直接记录敌人数,然后总人数减去敌人数再减1不就是朋友数了吗?至于快速找到唯一的朋友,可以考虑用二维数组vis[1001][1001]来直接记录,同时用一个链表来实时更改朋友链接(即快速由一个朋友转移到另一个朋友),可以桶排序后O(n*n)来解决。就可以以整体效率O(n*n*t)来完成本题。

参考代码

题解

  如果说只有长度一个条件,这道题该如何处理?如果先按照长度升序排序,就能采用拦截导弹贪心的思想,每一次用最大满足条件的数来继承,如果一个都不满足条件就ans++。用这种方式是能够保证正确性的,因为对于这个数而言,达成的效果只可能是把一组数的最大值推到这个数,而由什么推过来却有选择,用这种贪心方式是能够让效益最大化。

参考代码

T6:问题 F: 双端队列

题目描述

达达现在碰到了一个棘手的问题,有N个整数需要排序。

达达手头能用的工具就是若干个双端队列。

她从1到N需要依次处理这N个数,对于每个数,达达能做以下两件事:

1.新建一个双端队列,并将当前数作为这个队列中的唯一的数;

2.将当前数放入已有的队列的头之前或者尾之后。

对所有的数处理完成之后,达达将这些队列按一定的顺序连接起来后就可以得到一个非降的序列。

请你求出最少需要多少个双端序列。

输入

第一行输入整数N,代表整数的个数。

接下来N行,每行包括一个整数Di,代表所需处理的整数。

输出

输出一个整数,代表最少需要的双端队列数。

样例输入

6

3

6

0

9

6

3

样例输出

2

提示

【数据范围】

1≤N≤200000

题解

这道题,用模拟,绝对TLE。因此又要用一些奇奇怪怪的方法了。按照大小排序,然后下标跟着走(结构体排序),对于中间的一个数,后面编号比它大的可以放后面,前面编号比它大的可以放前面,由此就是求“谷”。对于相同的数不同的下标,可以缩减成一个数,既可以作为上升段也可以作为下降段,关键在于做哪一段更优,即代码中对于j的变化,i到j-1即为值相等下标不等的“数”。现在再来看排序的comp函数,首先数的大小由大到小,然后按照编号由小到大,无论怎么说,编号一定要由小到大!然后由于用这种方式记录答案是不包含第一个的(判断是没法判断第一个是转折点,前面没东西了),所以答案要+1,就算啥也没有(平的)答案也是1。

参考代码

#include<algorithm> 
#include<cstdio>
using namespace std;
struct node
{
	int num;
	int val;
}a[200100];
bool comp1(node p,node q)
{ return p.val>q.val||p.val==q.val&&p.num<q.num; }
int n,pd=-1,pre=999999999,ans=0;
int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)
	{
		scanf("%d",&a[i].val);
		a[i].num=i;
	}
	sort(a,a+n,comp1);
	for(int i=0;i<n;)
	{
		int j=i;
		while(j<n&&a[j].val==a[i].val) j++;
		if(pd==-1)
		{
			if(a[j-1].num<pre) pre=a[i].num;
			else
			{
				pd=1;
				pre=a[j-1].num;
			}
		}
		else
		{
			if(a[i].num>pre) pre=a[j-1].num;
			else
			{
				pd=-1;
				ans++;
				pre=a[i].num;
			}
		}
		i=j;
	}
	printf("%d",ans+1);
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值