19级HPU算法协会公开课第一期:【基础算法1】 题解

HPU算法协会公开课第一期:【基础算法1】

      • A - 前m大的数 (HDU - 1280)
        • 分析
        • 代码
      • B - 稳定排序 (HDU - 1872)
        • 分析
        • 代码
      • C - 开门人和关门人 (HDU - 1234)
        • 分析
        • 代码
      • D - EXCEL排序 (HDU - 1862)
        • 分析
        • 代码
      • E - {A} + {B} (HDU - 1412)
        • 分析
        • 代码
      • F - 水果 (HDU - 1263)
        • 分析
        • 代码
      • G - 不重复数字 (HYSBZ - 2761)
        • 分析
        • 代码
      • H - 表达式括号匹配 (计蒜客 - T1655)
        • 分析
        • 代码
      • I - 合并果子 (CSU - 1588)
        • 分析
        • 代码
      • J - Covered Points Count (CodeForces - 1000C)
        • 分析
        • 代码
      • K - Ignatius and the Princess IV (HDU - 1029)
        • 分析
        • 代码
      • L - Stones (HDU - 1896)
        • 分析
        • 代码
      • M - SnowWolf's Wine Shop (HDU - 1897)
        • 分析
        • 代码
      • N - Alice, Bob and Candies (CodeForces - 1352D)
        • 分析
        • 代码
      • O - Special Elements (CodeForces - 1352E)
        • 分析
        • 代码
      • P - Max Sum (HDU - 1003)

比赛链接
passwordHPUACM

A - 前m大的数 (HDU - 1280)

还记得Gardon给小希布置的那个作业么?(上次比赛的1005)其实小希已经找回了原来的那张数表,现在她想确认一下她的答案是否正确,但是整个的答案是很庞大的表,小希只想让你把答案中最大的M个数告诉她就可以了。
给定一个包含N(N<=3000)个正整数的序列,每个数不超过5000,对它们两两相加得到的N*(N-1)/2个和,求出其中前M大的数(M<=1000)并按从大到小的顺序排列。
Input
输入可能包含多组数据,其中每组数据包括两行:
第一行两个数N和M,
第二行N个数,表示该序列。
Output
对于输入的每组数据,输出M个数,表示结果。输出应当按照从大到小的顺序排列。
Sample Input
4 4
1 2 3 4
4 5
5 3 6 4
Sample Output
7 6 5 5
11 10 9 9 8

分析

刚看到这道题,发现正是上一次比赛的题,然后就把以前的代码又写了一遍,结果发现超时了_(°:з」∠)_然后问了一下大佬,原来是因为数组开很大的时候最好放在main函数的外面,不然就容易出错!于是这个故事告诉我们:
开变量最好开成全局变量!

代码

#include
#include
using namespace std;
int N,M,cnt,a[3001],b[5000000];//要记得把b开高点
int main()
{
	while(~scanf("%d%d",&N,&M))
	{
		cnt=0;
		for(int i=0;i<N;i++)
			scanf("%d",&a[i]);
		for(int i=0;i<N;i++)
		{
			for(int j=i+1;j<N;j++)
			{
				b[cnt++]=a[i]+a[j];
			}
		}
		sort(b,b+cnt);//sort排序后是从小到大的
		/*输出*/
		for(int i=cnt-1;i>=0;i--)
		{
			if(--M==0)
			{
				printf("%d\n",b[i]);
				break;
			}
			else	printf("%d ",b[i]);
		}
	}
}

最后的输出部分可以稍微简便一点:

		for(int i=cnt-1;i>cnt-M;i--)
			printf("%d ",b[i]);
		printf("%d\n",b[cnt-M]);

B - 稳定排序 (HDU - 1872)

大家都知道,快速排序是不稳定的排序方法。
如果对于数组中出现的任意a[i],a[j] (i
某高校招生办得到一份成绩列表,上面记录了考生名字和考生成绩。并且对其使用了某排序算法按成绩进行递减排序。现在请你判断一下该排序算法是否正确,如果正确的话,则判断该排序算法是否为稳定的。
Input
本题目包含多组输入,请处理到文件结束。
对于每组数据,第一行有一个正整数N(0 接下来有N行,每一行有一个字符串代表考生名字(长度不超过50,仅包含’a’~‘z’),和一个整数代表考生分数(小于500)。其中名字和成绩用一个空格隔开。
再接下来又有N行,是上述列表经过某排序算法以后生成的一个序列。格式同上。
Output
对于每组数据,如果算法是正确并且稳定的,就在一行里面输出"Right"。如果算法是正确的但不是稳定的,就在一行里面输出"Not Stable",并且在下面输出正确稳定排序的列表,格式同输入。如果该算法是错误的,就在一行里面输出"Error",并且在下面输出正确稳定排序的列表,格式同输入。
注意,本题目不考虑该排序算法是错误的,但结果是正确的这样的意外情况。
Sample Input
3
aa 10
bb 10
cc 20
cc 20
bb 10
aa 10
3
aa 10
bb 10
cc 20
cc 20
aa 10
bb 10
3
aa 10
bb 10
cc 20
aa 10
bb 10
cc 20
Sample Output
Not Stable
cc 20
aa 10
bb 10
Right
Error
cc 20
aa 10
bb 10

分析

这题一开始定义node和cmp函数的时候,没有考虑到题目中稳定的定义,因此只开了两个变量,实际上应该储存并判断数据的下标。

程序的思想:读入原始成绩列表+经过某算法排序后的成绩列表,然后将原始成绩列表按正确的排序方式进行排序,排序完成后再与经过某算法排序后的成绩列表进行比较,推断经过某算法排序后的成绩列表是“正确”/“不正确”/“不稳定”的。

代码

#include
#include
#include
#include
using namespace std;

struct node	//定义结构体
{
	string s1;	//s1存放考生姓名
	int s2,s3;	//s2存放考试成绩,s3存放考生下标
}nod1[301],nod2[301];	//nod1存放原始成绩列表,nod2存放经过某排序算法后的成绩列表

bool cmp(node a,node b)	//定义比较函数
{
	if(a.s2!=b.s2)	return a.s2>b.s2;	//如果成绩不同,则将成绩高的排到前面
	else	return a.s3<b.s3;	//如果成绩相同,则下标小的排前面
}

int N,flag1,flag2;
string str;
int score;

int main()
{
	while(cin>>N)	//多组输入
	{
		for(int i=0;i<N;i++)	//读入数据
		{
			cin>>str>>score;
			nod1[i].s1=str;
			nod1[i].s2=score;
			nod1[i].s3=i;
		}
		flag1=1,flag2=1;
		sort(nod1,nod1+N,cmp);	//将原始成绩列表按正确的排序方法排序
		for(int i=0;i<N;i++)
		{
			//读入数据
			cin>>str>>score;
			nod2[i].s1=str;
			nod2[i].s2=score;
			nod2[i].s3=i;
			//比较
			if(nod1[i].s2 != nod2[i].s2)	//如果经过某排序算法后的成绩顺序都不对
				flag1=0;	//则它铁定是错误的
			if(nod1[i].s1 != nod2[i].s1)	//如果经过某排序算法后的成绩下标不对
				flag2=0;	//则它是不稳定的
		}
		if(flag1+flag2==2)	//如果经过某排序算法后的成绩既不是错误的也不是不稳定的
		{
			cout<<"Right"<<endl;	//则它是正确的
		}
		else if(flag1==0)	//它是错误的
		{
			cout<<"Error"<<endl;
			for(int i=0;i<N;i++)//输出正确的排序顺序
			{
				cout<<nod1[i].s1<<' '<<nod1[i].s2<<endl;
			}
		}
		else if(flag2==0)	//它是不稳定的
		{
			cout<<"Not Stable"<<endl;
			for(int i=0;i<N;i++)	//输出正确的排序顺序
			{
				cout<<nod1[i].s1<<' '<<nod1[i].s2<<endl;
			}
		}
	}
}

C - 开门人和关门人 (HDU - 1234)

每天第一个到机房的人要把门打开,最后一个离开的人要把门关好。现有一堆杂乱的机房签到、签离记录,请根据记录找出当天开门和关门的人。
Input
测试输入的第一行给出记录的总天数N ( > 0 )。下面列出了N天的记录。
每天的记录在第一行给出记录的条目数M ( > 0 ),下面是M行,每行的格式为
证件号码 签到时间 签离时间
其中时间按“小时:分钟:秒钟”(各占2位)给出,证件号码是长度不超过15的字符串。
Output
对每一天的记录输出1行,即当天开门和关门人的证件号码,中间用1空格分隔。
注意:在裁判的标准测试输入中,所有记录保证完整,每个人的签到时间在签离时间之前,且没有多人同时签到或者签离的情况。
Sample Input
3
1
ME3021112225321 00:00:00 23:59:59
2
EE301218 08:05:35 20:56:35
MA301134 12:35:45 21:40:42
3
CS301111 15:30:28 17:00:10
SC3021234 08:00:00 11:25:25
CS301133 21:45:00 21:58:40
Sample Output
ME3021112225321 ME3021112225321
EE301218 MA301134
SC3021234 CS301133

分析

这题的思想不难,就是储存每个人的证件号码,以及其对应的签到和签离时间,最后只需要选出签到时间最早(数值最小)的人,和签离时间最晚(数值最大)的人,分别输出他们的证件号码就行了。

代码

#include
#include
#include
#include
using namespace std;

struct node	//定义结构体
{
	string s1,s2,s3;	//s1存放证件号码,s2存放签到时间,s3存放签离时间
}nod[1000];

bool cmp1(node a,node b)	//签到时间早的排前面
{
	return a.s2<b.s2;
}

bool cmp2(node a,node b)	//签离时间晚的放前面
{
	return a.s3>b.s3;
}

string st1,st2,st3;
int N,M;

int main()
{
	cin>>N;
	for(int i=0;i<N;i++)	//N天的记录
	{
		cin>>M;
		for(int j=0;j<M;j++)//输入数据
		{
			cin>>st1>>st2>>st3;
			nod[j].s1=st1;
			nod[j].s2=st2;
			nod[j].s3=st3;
		}
		//输出签到时间最早的人的证件号码
		sort(nod,nod+M,cmp1);
		cout<<nod[0].s1<<' ';
		//输出签离时间最晚的人的证件号码
		sort(nod,nod+M,cmp2);
		cout<<nod[0].s1<<endl;
	}
}

D - EXCEL排序 (HDU - 1862)

Excel可以对一组纪录按任意指定列排序。现请你编写程序实现类似功能。
Input
测试输入包含若干测试用例。每个测试用例的第1行包含两个整数 N (<=100000) 和 C,其中 N 是纪录的条数,C 是指定排序的列号。以下有 N 行,每行包含一条学生纪录。每条学生纪录由学号(6位数字,同组测试中没有重复的学号)、姓名(不超过8位且不包含空格的字符串)、成绩(闭区间[0, 100]内的整数)组成,每个项目间用1个空格隔开。当读到 N=0 时,全部输入结束,相应的结果不要输出。
Output
对每个测试用例,首先输出1行“Case i:”,其中 i 是测试用例的编号(从1开始)。随后在 N 行中输出按要求排序后的结果,即:当 C=1 时,按学号递增排序;当 C=2 时,按姓名的非递减字典序排序;当 C=3 时,按成绩的非递减排序。当若干学生具有相同姓名或者相同成绩时,则按他们的学号递增排序。
Sample Input
3 1
000007 James 85
000010 Amy 90
000001 Zoe 60
4 2
000007 James 85
000010 Amy 90
000001 Zoe 60
000002 James 98
4 3
000007 James 85
000010 Amy 90
000001 Zoe 60
000002 James 90
0 0
Sample Output
Case 1:
000001 Zoe 60
000007 James 85
000010 Amy 90
Case 2:
000010 Amy 90
000002 James 98
000007 James 85
000001 Zoe 60
Case 3:
000001 Zoe 60
000007 James 85
000002 James 90
000010 Amy 90

分析

输入C的值后,对输入的数据进行其对应的操作。此处要注意:①C=2时,按姓名的非递减字典序排序,非递减其实就是递增,但是因为可能出现相同的值(姓名),因此不是完全意义上的“递增”;② C=3 时,按成绩的非递减排序,道理同①,实际也是递增的意思;③当若干学生具有相同姓名或者相同成绩时,则按他们的学号递增排序。

代码

#include
#include
#include
#include
using namespace std;

struct node	//定义结构体
{
	string s1,s2;	//s1存放学号,s2存放姓名
	int s3;	//s3存放成绩
}nod[100001];

bool cmp1(node a,node b)	//C=1时,按学号递增排序
{
	return a.s1<b.s1;
}

bool cmp2(node a,node b)	//C=2时,按姓名的非递减字典序排序
{
	if(a.s2==b.s2)	return a.s1<b.s1;	//当具有相同姓名时,按学号递增排序
	else	return a.s2<b.s2;	//否则按姓名的递减字典序排序
}

bool cmp3(node a,node b)	//C=3时,按成绩的非递减排序
{
	if(a.s3==b.s3)	return a.s1<b.s1;	//当具有相同成绩时,按学号递增排序
	else	return a.s3<b.s3;	//否则按成绩的递减字典序排序
}

int N,C,num=1;	//num表示测试用例的编号
string x,y;
int z;

int main()
{
	while(cin>>N>>C)
	{
		if(N==0 && C==0)	break;
		//输入数据
		for(int i=0;i<N;i++)
		{
			cin>>x>>y>>z;
			nod[i].s1=x;
			nod[i].s2=y;
			nod[i].s3=z;
		}
		//按照C的值进行对应的排序
		if(C==1)
		{
			sort(nod,nod+N,cmp1);
		}
		else if(C==2)
		{
			sort(nod,nod+N,cmp2);
		}
		else if(C==3)
		{
			sort(nod,nod+N,cmp3);
		}
		//输出
		cout<<"Case "<<num<<':'<<endl;
		num++;
		for(int i=0;i<N;i++)
		{
			cout<<nod[i].s1<<' '<<nod[i].s2<<' '<<nod[i].s3<<endl;
		}
	}
}

E - {A} + {B} (HDU - 1412)

给你两个集合,要求{A} + {B}.
注:同一个集合中不会有两个相同的元素.
Input
每组输入数据分为三行,第一行有两个数字n,m(0 Output
针对每组数据输出一行数据,表示合并后的集合,要求从小到大输出,每个元素之间有一个空格隔开。
Sample Input
1 2
1
2 3
1 2
1
1 2
Sample Output
1 2 3
1 2

分析

由于set函数有排序(自小到大排序)+去重的功能,因此此题考虑使用set。

这题主要是输出时有点迷,如果输出时不加上*,输出的就是错误的。查了一下发现set中的begin()返回的是第一个元素的迭代器,end()返回的是最后一个元素的迭代器(迭代器与指针的用法类似),因此需要加上*。

代码

#include
#include
using namespace std;
int n,m,t;
set<int> A;
int main()
{
	while(~scanf("%d%d",&n,&m))	//输入集合A的个数n和集合B的个数m
	{
		A.clear(); 	//每次输入数据都清空A
		for(int i=0;i<n;i++)	//输入集合A中的元素
		{
			scanf("%d",&t);
			A.insert(t);	//将集合A中的元素塞进去
		}
		for(int i=0;i<m;i++)	//输入集合B中的元素
		{
			scanf("%d",&t);
			A.insert(t);	//将集合B中的元素塞进去
		}
		/*输出*/
		printf("%d",*A.begin());	//输出塞完+去重完+排序完后的集合的第一个元素
		A.erase(A.begin());	//擦掉集合的第一个元素
		while(!A.empty())	//只要集合里还有元素(集合不为空)
		{
			//每次输出集合的第一个元素,输出完后将第一个元素擦掉
			printf(" %d",*A.begin());
			A.erase(A.begin());
		}
		printf("\n");
	}
}

既然set中的begin()end()返回的都是迭代器,可以试一试迭代器的做法:

  • 先在最开头定义一下全局变量
set<int> A;
set<int>::iterator it;
  • 其他位置无需改变,只需要改动一下最后的输出部分
		it=A.begin();
		printf("%d",*it);
		it++;
		for(;it!=A.end();it++)
			printf(" %d",*it);
		printf("\n");

F - 水果 (HDU - 1263)

夏天来了~ 好开心啊,呵呵,好多好多水果 ~
Joe经营着一个不大的水果店。他认为生存之道就是经营最受顾客欢迎的水果。现在他想要一份水果销售情况的明细表,这样Joe就可以很容易掌握所有水果的销售情况了。
Input
第一行正整数N(0 每组测试数据的第一行是一个整数M(0 Output
对于每一组测试数据,请你输出一份排版格式正确(请分析样本输出)的水果销售情况明细表。这份明细表包括所有水果的产地,名称和销售数目的信息。水果先按产地分类,产地按字母顺序排列;同一产地的水果按照名称排序,名称按字母顺序排序.
两组测试数据之间有一个空行。最后一组测试数据之后没有空行。
Sample Input
1
5
apple shandong 3
pineapple guangdong 1
sugarcane guangdong 1
pineapple guangdong 3
pineapple guangdong 1
Sample Output(输出的|----前面有3个空格)
guangdong
|----pineapple(5)
|----sugarcane(1)
shandong
|----apple(3)

分析

这题需要将一次交易的三个信息都存储起来,并把相同产地和类型的水果的销售数目相加。排序问题不难从例子中看出,只是需要注意输出格式+“最后一组测试数据之后没有空行”。

代码

#include
#include
#include
#include
using namespace std;
struct node	//定义结构体
{
	string fruit,area;	//fruit存放水果的名字,area存放水果的产地
	/*重载<操作符,可以对两个node使用<操作符进行比较*/
	bool operator < (const node & other) const
	{	//括号中的const表示参数other对象不会被修改,最后的const表明调用函数对象不会被修改
		if(area == other.area)	return fruit<other.fruit;	//如果产地相同,将水果名字按递增字典序排序
		else return area<other.area;	//如果产地不同,将产地名字按递增字典序排序
	}
};

map<node,int> mp;
map<node,int>::iterator it;
node nod;
int N,M;
string u,v,Are;
int w;

int main()
{
	cin>>N;
	for(int i=0;i<N;i++)	//有N组测试数据
	{
		mp.clear();	//每组数据运行前,清空上次的mp
		cin>>M;
		for(int j=0;j<M;j++)	//输入M次交易成功的数据
		{
			cin>>u>>v>>w;
			nod.fruit=u;
			nod.area=v;
			mp[nod]=mp[nod]+w;	//若是当前进行的交易与之前进行的交易是同一个产地的同一个水果,就将销售数目相加
		}
		it=mp.begin();	//迭代器it指向mp的第一个元素
		Are=it->first.area;
		cout<<Are<<endl;	//输出mp中第一个元素的产地
		for(;it!=mp.end();it++)
		{
			if(it->first.area != Are)	//如果it现在指向的键中的水果产地与之前存放的水果产地不同
			{
				Are=it->first.area;
				cout<<Are<<endl;	//输出现在的水果产地
			}
			cout<<"   |----"<<it->first.fruit<<'('<<it->second<<')'<<endl;//输出键中的水果名称和值
		}
		if(i==N-1);
		else	cout<<endl;	//如果现在不是最后一组测试数据,就回车
	}
}

G - 不重复数字 (HYSBZ - 2761)

给出N个数,要求把其中重复的去掉,只保留第一次出现的数。
例如,给出的数为1 2 18 3 3 19 2 3 6 5 4,其中2和3有重复,去除后的结果为1 2 18 3 19 6 5 4。
Input
输入第一行为正整数T,表示有T组数据。
接下来每组数据包括两行,第一行为正整数N,表示有N个数。第二行为要去重的N个正整数。
Output
对于每组数据,输出一行,为去重后剩下的数字,数字之间用一个空格隔开。
Sample Input
2
11
1 2 18 3 3 19 2 3 6 5 4
6
1 2 3 4 5 6
Sample Output
1 2 18 3 19 6 5 4
1 2 3 4 5 6
Hint
对于30%的数据,1 <= N <= 100,给出的数不大于100,均为非负整数;
对于50%的数据,1 <= N <= 10000,给出的数不大于10000,均为非负整数;
对于100%的数据,1 <= N <= 50000,给出的数在32位有符号整数范围内。
提示:
由于数据量很大,使用C++的同学请使用scanf和printf来进行输入输出操作,以免浪费不必要的时间。

分析

看到去重第一个反应是用set,但是set会从小到大排序,难以按原先的输入顺序进行输出。因此可以考虑使用map

代码

#include
#include
using namespace std;
map<int,int> mp;
int T,N,t,n;

int main()
{
	scanf("%d",&T);
	for(int i=0;i<T;i++)	//有T组数据
	{
		scanf("%d",&N);
		n=0;
		mp.clear();	//每组数据运行前,清空上次的mp
		for(int j=0;j<N;j++)	//有N个数
		{
			scanf("%d",&t);
			if(mp[t]==0)	//如果之前没有存放过数据t
			{
				mp[t]=1;	//把数据t标记成“已存”状态
				if(n==0)	//如果是第一个数(格式化输出)
				{
					printf("%d",t);
					n=1;
				}
				else	printf(" %d",t);	//如果不是第一个数(格式化输出)
			}
		}
		printf("\n");
	}
}

H - 表达式括号匹配 (计蒜客 - T1655)

给出一个表达式,该表达式仅由字符(、)、+、-以及数字组成。
请编写一个程序检查表达式中的左右圆括号是否匹配,若匹配,则返回"YES";否则返回"NO"。
输入格式
一行一个表达式,长度不超过 2000。
输出格式
若匹配,则输出"YES";否则输出"NO"
Sample Input
2+(3-4)-2-6)
Sample Output
NO

分析

这道题由于只问括号的匹配情况,因此我们也只考虑括号的状况而不考虑其他符号的匹配情况。括号的情况有以下几种:
19级HPU算法协会公开课第一期:【基础算法1】 题解_第1张图片
①是括号正确匹配的情况;②是左括号(比右括号)多的情况;③是右括号)比左括号(多的情况;④是左括号(比右括号)多的特殊情况;⑤是右括号)比左括号(多的特殊情况。
因此遇到左括号就flag+1,遇到右括号就flag-1。
如果字符串遍历结束后flag为正数,说明左括号比右括号多(②④情况);
如果字符串遍历结束后flag为负数,说明右括号比左括号多(③⑤情况);
如果字符串在遍历过程中有flag为负数的情况,说明字符串中间就有右括号比左括号多的情况了,一定是错误的,直接跳出循环不再遍历。

代码

  • 老实人写法

    #include
    #include
    #include
    using namespace std;
    
    string str;
    int flag=0;
    
    int main()
    {
    	cin>>str;	//读入字符串
    	for(int i=0;i<str.size();i++)	//遍历字符串
    	{
    		if(str[i]=='(')	//如果遇见了左括号就flag++
    		{
    			flag++;
    		}
    		else if(str[i]==')')	//如果遇见了右括号就flag--
    		{
    			flag--;
    		}
    		if(flag<0)	//如果在遍历过程中已经出现了右括号多于左括号的情况,一定是错误的,直接输出并跳出循环
    		{
    			printf("NO\n");
    			break;
    		}
    	}
    	if(flag>0)	printf("NO\n");	//左括号多于右括号
    	else if(flag==0)	printf("YES\n");	//左右括号相互匹配
    }
    
  • 利用栈stack的写法

    #include
    #include
    #include
    #include
    using namespace std;
    
    stack<char> s;
    string str;
    int len;
    
    int main()
    {
    	cin>>str; 	//读入字符串
    	len=str.size();
    	for(int i=0;i<len;i++)
    	{
    		if(str[i]=='(')	s.push(str[i]);
    		if(str[i]==')')
    		{
    			if(!s.empty())
    			{
    				if(s.top()=='(')	s.pop();
    				else	s.push(str[i]);
    			}
    			else	s.push(str[i]);
    		}
    	}
    	if(s.empty())	cout<<"YES"<<"\n";
    	else	cout<<"NO"<<"\n";
    }
    

I - 合并果子 (CSU - 1588)

现在有n堆果子,第i堆有ai个果子。现在要把这些果子合并成一堆,每次合并的代价是两堆果子的总果子数。求合并所有果子的最小代价。
Input
第一行包含一个整数T(T<=50),表示数据组数。
每组数据第一行包含一个整数n(2<=n<=1000),表示果子的堆数。
第二行包含n个正整数ai(ai<=100),表示每堆果子的果子数。
Output
每组数据仅一行,表示最小合并代价。
Sample Input
2
4
1 2 3 4
5
3 5 2 1 4
Sample Output
19
33

分析

由于要求合并果子的最小代价,因此一定是每次都排序好了之后,选择个数最少的两堆果子进行合并,再把合并过后的果子作为新的一堆与剩下的几堆进行排序,重复操作直到所有果子都合并完为止。
举个例子:
19级HPU算法协会公开课第一期:【基础算法1】 题解_第2张图片

代码

#include
#include
#include
using namespace std;

int T,n,a[1001],sum,cnt; 

int main()
{
	scanf("%d",&T);
	for(int i=0;i<T;i++)	//有T组数据
	{
		scanf("%d",&n);
		for(int j=0;j<n;j++)	//读入每堆果子的果子数
		{
			scanf("%d",&a[j]);
		}
		/*合并果子*/
		sum=0;	//计算总的合并代价
		for(int j=0;j<n-1;j++)
		{
			sort(a+j,a+n);	//从小到大排序
			cnt=a[j]+a[j+1];	//当前的合并代价是最前面两个果子数相加
			sum=sum+cnt;	//更新总的合并代价
			a[j+1]=cnt;	//将下一个循环使用的数组最开头的数变成两个果子数的总数
		}
		printf("%d\n",sum);
	}
}

J - Covered Points Count (CodeForces - 1000C)

这题原本是全英文,引用英文也没啥意思(可能还是会跑去用百度翻译),所以就把意思写一下。
You are given n segments on a coordinate line; each endpoint of every segment has integer coordinates.

在一条坐标线上有n个线段;每条线段的每个端点都有整数坐标。某些线段可以退化成点。线段可以相互相交、嵌套甚至重合。
您的任务如下:对于每个k∈[1…n],计算具有整数坐标的点的数目,使得覆盖这些点的线段的数目等于k。当且仅当li≤x≤ri时,端点为li和ri的线段覆盖x点。
Input
输入的第一行包含一个整数n(1≤n≤2⋅105),表示有n段。
接下来的n行包含段。第i行包含一对整数li,ri(0≤li≤ri≤1018) 表示第i段的端点。
Output
打印n个空格分隔的整数cnt1、cnt2、…、cntn,其中cnti等于点的数目,覆盖这些点的段的数目等于i。
Examples
Input
3
0 3
1 3
3 8
Output
6 2 1
19级HPU算法协会公开课第一期:【基础算法1】 题解_第3张图片
Input
3
1 3
2 4
5 7
Output
5 2 0
19级HPU算法协会公开课第一期:【基础算法1】 题解_第4张图片

分析

这题差分的思想。由于0≤li≤ri≤1018,开这么大的数组显然不合适,因此考虑差分。

差分是用数组来维护第i个数和其前一个数的差值的办法。当需要将[li,ri]区间的每一个数+ki时,只需要修改第[li](把它+1)和第[ri+1](把它-1)的值。

代码

#include
#include
#include
#include
using namespace std;

int n;
long long l,r,sum[400002],last,cnt;

struct node
{
	long long p;	//p储存节点的下标 
	int k;	//k储存+1还是-1 
};

bool cmp(node u,node v)
{
	return u.p==v.p?u.k<v.k:u.p<v.p;
}

vector<node> q;	//用vector保存节点

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)	//有n段数据
	{
		cin>>l>>r;
		q.push_back((node){l,1});	//左端点+1
		q.push_back((node){r+1,-1});	//右端点的后一位-1
	}
	sort(q.begin(),q.end(),cmp);	//排序
	
	cnt=0,last=0;	//点被覆盖cnt次,上一个节点是last,sum[cnt]代表被覆盖cnt次的点数
	for(int i=0;i<q.size();i++)
	{
		sum[cnt]+=q[i].p-last;
		cnt+=q[i].k;
		last=q[i].p;
	} 
	/*输出*/
	for(int i=1;i<=n;i++)
	{
		cout<<sum[i]<<" ";
	}
}

K - Ignatius and the Princess IV (HDU - 1029)

这题也是英文题哈,然后还是把题意写一下:“OK, you are not too bad, em… But you can never pass the next test.” feng5166 says.

他告诉你一个奇数N,之后是N个整数,其中有一个特殊的整数,在告诉你所有的整数后,你需要告诉他哪个整数是特殊的。
特殊的整数的特征是:该整数至少出现(N+1)/2次。
Input
输入包含几个测试用例。每个测试用例包含两行。第一行由一个奇数N(1<=N<=999999)组成,它表示他将告诉我们的整数的个数。第二行包含N个整数。输入在文件结束时终止。
Output
对于每个测试用例,您只需要输出一行,其中包含您找到的特殊编号。
Sample Input
5
1 3 2 3 3
11
1 1 1 1 1 5 5 5 5 5 5
7
1 1 1 1 1 1 1
Sample Output
3
5
1

分析

第一个反应是用map,键存放输入的整数,其对应的值是它出现的次数。由于只有一个特殊的整数,就只要在输入的数据中找到出现的次数大于等于(N+1)/2的数,并把它的信息记录下来就可以了。

实际上用数组也能实现这样的效果。

代码

  • map做法

    #include
    #include
    using namespace std;
    
    int N,n,m,t;
    map<int,int> mp;
    
    int main()
    {
    	while(~scanf("%d",&N))	//有多组输入
    	{
    		mp.clear();	//每组数据运行前,清空上次的mp
    		n=(N+1)/2;
    		for(int i=0;i<N;i++)	//输入N个整数
    		{
    			scanf("%d",&t);
    			mp[t]++;
    			if(mp[t]>=n)	//如果t出现的次数>=(N+1)/2
    				m=t;	//它是特殊的数,记录下来
    		}
    		printf("%d\n",m);
    	}
    }
    
  • 数组做法
    定义一下全局变量

    int mp[1000000];
    

    其他只要省去mp.clear()就行了。
    整个代码如下:

    #include
    using namespace std;
    
    int N,n,m,t;
    int mp[1000000];
    
    int main()
    {
    	while(~scanf("%d",&N))
    	{
    		n=(N+1)/2;
    		for(int i=0;i<N;i++)
    		{
    			scanf("%d",&t);
    			mp[t]++;
    			if(mp[t]>=n)
    				m=t;
    		}
    		printf("%d\n",m);
    	}
    }
    

L - Stones (HDU - 1896)

英文题*3:Because of the wrong status of the bicycle, Sempr begin to walk east to west every morning and walk back every evening.

路上有许多石头,当Sempr遇到一块石头时,如果是他遇到的奇石,他会尽量把它扔到前面,如果是偶数石,他会把它留在原来的地方。现在给你一些关于路上石头的信息,你要告诉我从起点到最远的石头的距离。请注意,如果两个或两个以上的石头停留在同一位置,您将首先遇到较大的石头(如输入中所述,具有最小Di的石头)。
Input
在第一行中,有一个整数T(1<=T<=10),表示输入文件中的测试用例。然后是T测试用例。
对于每个测试用例,我将在第一行给出一个整数N(0i(0<=Pi<=100000)和Di(0<=Di<=1000),这意味着第i块石头的位置和Sempr可以扔多远。
Output
只需为一个测试用例输出一行,如描述中所述。
Sample Input
2
2
1 5
2 4
2
1 5
6 6
Sample Output
11
12

分析

由题目可知:当操作数是奇数次时,要把石头扔出去;操作数是偶数次时,就不把石头扔出去。
当是最后一次操作时,计算最后一次石头扔出去的距离,输出结果。

代码

#include
#include
#include
using namespace std;

int T,N,a,b,cnt;

struct node
{
	int p,d;	//p存储石头当前位置,d存储石头可以被扔多远
	//由于优先队列是从大到小排,此处重载小于号,让它变成从小到大排
	bool operator < (const node & other)const
	{
		if(p != other.p)	return p>other.p;
		else	return d>other.d;
	}
}k;

int main()
{
	scanf("%d",&T);
	priority_queue<node> s;
	for(int i=0;i<T;i++)	//有T组测试用例
	{
		scanf("%d",&N);
		for(int j=0;j<N;j++)	//有N块石头
		{
			scanf("%d%d",&a,&b);
			s.push({a,b});
		}
		cnt=1;
		while(s.size()>1)	//不是最后一次操作
		{
			k=s.top();	//目前先遇上的
			s.pop();	//扔掉这块石头
			if(cnt%2==1)	//遇到的是奇石(是第奇数次操作)
				s.push({k.p+k.d,k.d});
			cnt++;
		}
		printf("%d\n",s.top().p+s.top().d);	//输出从起点到最远的石头的距离
		s.pop(); 	//扔掉这块石头
	}
}

M - SnowWolf’s Wine Shop (HDU - 1897)

英文题*4:After retired from hustacm, Sempr(Liangjing Wang) find a part-time-job in Snowwolf(Wei Xu)'s wine shop.

商店里的葡萄酒质量各不相同,而且都有一个描述质量的等级,所有的等级都大于0且小于1,000,000。每天有Q个人去店里买酒。如果一个客顾客想要X品级的酒,但是店里没有正好是X的酒,店员就会卖给他一个比X更好的最小评级的酒(即卖出品级最小的 大于等于X品级 的酒),但是卖出的酒与顾客实际想要的品级之差不得超过Y。每天店员都会放出N瓶不同或相同质量的葡萄酒拿来卖。
Input
第一行有一个整数T(0 对于每个测试用例:
在第一行,有3个整数,N,Q和Y。
第二行是N个整数,表示每瓶葡萄酒的质量等级。
在第三行中,有Q个整数,意味着从早到晚的客户的请求。
这里,所有的整数都在0到1,000,000 之间。
Output
对于每个测试用例,输出“Case I:”,其中I是用例号,然后是Q个不同的整数行,这意味着葡萄酒的质量Sempr将在该时间销售。如果他卖不出酒,就出-1。
Sample Input
2
2 3 3
2 3
1 2 3
2 3 0
2 3
3 1 2
Sample Output
Case 1:
2
3
-1
Case 2:
3
-1
2

分析

这题向大佬学习了一个新知识——multiset
找到一个multiset的用法总结链接:网页链接

set是从小到大排序并且去重,multiset是从小到大排序但不去重。正好此题中酒的质量可能是相等的,不能去重;而需要排序是因为要找到比X更好的最小评级的酒,排序后容易寻找,更重要的是multiset拥有一个特殊的搜寻函数——lower_bound(elem),它可以返回元素值为elem的第一个可安插位置,也就是元素值 >= elem的第一个元素位置,与题意非常相符。因此这里选择使用multiset进行排序不去重操作。

代码

#include
#include
using namespace std;

int T,N,Q,Y;
int u,v,cnt;
multiset<int> mt;

int main()
{
	scanf("%d",&T);
	cnt=1;
	while(T--)	//有T个测试用例
	{
		scanf("%d%d%d",&N,&Q,&Y);	//有N瓶酒,Q个人,最多亏Y
		for(int i=0;i<N;i++)	//将N瓶酒的品质都塞进mt中
		{
			scanf("%d",&u);
			mt.insert(u);
		}
		for(int i=0;i<Q;i++)
		{
			scanf("%d",&v);	//读取顾客想要的酒的品质
			auto it=mt.lower_bound(v);	//让it指向>=顾客需要的品质的第一个元素
			if(i==0)	//如果这是第一个顾客
			{
				printf("Case %d:\n",cnt);	//先输出这是第几个用例
				cnt++;
			}
			if(it==mt.end() || *it-v>Y)//如果没有找到>=顾客需要的品质的元素,it会指向mt.end();如果it指向的元素的值与顾客需要的值相差超过最高限度Y
				printf("-1\n");	//不会出售酒
			else	//否则能出售酒
			{
				printf("%d\n",*it);
				mt.erase(it);	//由于it指向的酒被卖出,因此擦除该酒
			}
		}
	}
}

这里有一些需要注意的点:

  • lower_bound(elem)
    它返回元素值为elem的第一个可安插位置,也就是元素值 >= elem的第一个元素位置。它相当于指向>=elem的第一个元素的迭代器。

  • auto it=mt.lower_bound(v)
    auto作为一个关键字,在声明变量的时候会根据变量初始值的类型自动为此变量选择匹配的类型。而因为it后面等于的是一个迭代器,因此it是一个迭代器。
    如果不使用auto,也可以在一开始定义一个迭代器,即先:

    multiset<int>::iterator it;
    

    然后把 auto it=mt.lower_bound(v) 出现的地方替换成:

    			it=mt.lower_bound(v);
    

    就可以辽。

  • mt.end()
    mt.end()会返回一个随机存取迭代器,指向最后一个元素的下一个位置。如果到这一个位置的话就说明在前面没有找到相匹配的数。

  • mt.erase(it)
    根据上面贴出的网页链接,erase有三个用法
    ①mt.erase(elem):删除与elem相等的所有元素,返回被移除的元素个数。
    ②mt.erase(pos):移除迭代器pos所指位置元素,无返回值。
    ③mt.erase(beg,end):移除区间[beg,end)所有元素,无返回值。
    此处括号里写it,it是迭代器,因此这句话的意思是移除it指向的元素。如果是*it就会把与it对应的元素的值相等的元素都删除了,这样就题意不符了。


N - Alice, Bob and Candies (CodeForces - 1352D)

英文题*5:There are n candies in a row, they are numbered from left to right from 1 to n.

一排有n个糖果,从左到右从1到n编号。第i个糖果的大小是ai
Alice和Bob玩一个有趣又美味的游戏:吃糖果。Alice从左到右吃糖果,Bob从右到左吃。如果所有的糖果都吃光了,游戏就结束了。
这个过程由动作组成。在移动过程中,玩家从她/他身边吃一个或多个糖果(Alice从左边吃,Bob从右边吃)。
Alice第一个行动。在第一步中,她将吃一颗糖果(它的大小是a1)。然后,每个玩家交替连续移动——也就是说,Bob做第二个移动,然后Alice,然后再次Bob等。
每次移动时,玩家都会计算当前移动期间所吃糖果的总大小。一旦这个数字严格地大于其他玩家在上一次移动中所吃糖果的总大小,当前玩家停止进食,移动结束。换言之,在移动中,一个玩家吃尽可能少的糖果,使得在移动中所吃糖果的大小之和严格大于另一个玩家在上一个移动中所吃糖果的大小之和。如果没有足够的糖果来做这样的动作,那么玩家吃掉所有剩余的糖果,游戏结束。
例如,如果n=11,a=[3,1,4,1,5,9,2,6,5,3,5],那么:
· 第一步:Alice吃了一颗大小为3的糖果,糖果的顺序变成了[1,4,1,5,9,2,6,5,3,5]。
· 第二步:在前一步,Alice吃了3号,这意味着Bob必须吃4号或更多。Bob吃了一块5号的糖果,糖果的顺序变成了[1,4,1,5,9,2,6,5,3]。
· 第三步:Bob在前一步吃了5号,这意味着Alice必须吃6号或更多。Alice吃了三块总尺寸为1+4+1=6的糖果,糖果的顺序变成了[5,9,2,6,5,3]。
· 第四步:Alice在前一步中吃了6号,这意味着Bob必须吃7号或更多。Bob吃了两块总尺寸为3+5=8的糖果,糖果的顺序变成了[5,9,2,6]。
· 第五步:Bob在前一步吃了8号,这意味着Alice必须吃9个或更多。Alice吃了两块总尺寸为5+9=14的糖果,糖果的顺序变成了[2,6]。
· 第六步(最后一步):Alice在前一步吃了14个,这意味着Bob必须吃15个或更多。这是不可能的,所以Bob吃了剩下的两块糖果,比赛就结束了。
打印游戏中的移动次数和两个数字:
· a-Alice在游戏中吃的所有糖果的总大小;
· b-Bob在游戏中吃的所有糖果的总大小。
Input
第一行包含整数t(1≤t≤5000)——输入中的测试用例数。下面是对t测试用例的描述。
每个测试用例由两行组成。第一行包含整数n(1≤n≤1000)——糖果的数量。第二行包含一个整数序列a1,a2,…,an(1≤ai≤1000)——糖果的大小按从左到右的顺序排列。
保证所有输入数据组的n值之和不超过2⋅105
Output
对于每一组输入数据,打印三个整数——游戏中的移动次数以及所需的值a和b。
Example
Input
7
11
3 1 4 1 5 9 2 6 5 3 5
1
1000
3
1 1 1
13
1 2 3 4 5 6 7 8 9 10 11 12 13
2
2 1
6
1 1 1 1 1 1
7
1 1 1 1 1 1 1
Output
6 23 21
1 1000 0
2 1 2
6 45 46
2 2 1
3 4 2
4 4 3

分析

这题主要是使用deque进行双端插入和删除数据的操作。
deque的用法链接:网页链接

代码

#include
#include
#include
using namespace std;

int T,N,sum,cnt,t;
int sum1,sum2,nex;

int main()
{
	scanf("%d",&T);
	while(T--)	//有T组测试用例
	{
		scanf("%d",&N);
		deque<int> a;	//a存储糖果信息
		sum1=sum2=0;
		for(int i=0;i<N;i++)	//有N个糖果
		{
			scanf("%d",&t);
			a.push_back(t);
		}
		nex=a.front();	//记录其他玩家上一次移动中所吃糖果的总大小
		sum1+=nex;	//sum1存储Alice吃的糖果的总大小
		a.pop_front();	//扔掉a首部的糖果信息
		cnt=1;	
		while(!a.empty())	//糖果没有被吃完
		{
			sum=0;	//临时存储玩家吃了多少
			cnt++;
			if(cnt%2==1)	//Alice的回合
			{
				while(sum<=nex && !a.empty())	//如果玩家已经比其他玩家上次吃的多了,或者已经没有糖果了就不做
				{
					sum+=a.front();
					a.pop_front();
				}
				sum1+=sum;	//更新Alice吃的糖果的总大小
				nex=sum;	//更新其他玩家上一次移动中所吃糖果的总大小
			}
			else	//Bob的回合
			{
				while(sum<=nex && !a.empty())
				{
					sum+=a.back();
					a.pop_back();
				}
				sum2+=sum;	//更新Bob吃的糖果的总大小
				nex=sum;	//更新其他玩家上一次移动中所吃糖果的总大小
			}	
		}
		printf("%d %d %d\n",cnt,sum1,sum2);
	} 
}

O - Special Elements (CodeForces - 1352E)

英文题*6:Pay attention to the non-standard memory limit in this problem.

注意这个问题中的非标准内存限制。
在这个问题上,为了将有效解与无效解截然分开,时间限制相当严格。更喜欢使用编译的静态类型语言(例如C++)。如果您使用Python,那么就在PyPy上提交解决方案。试着写一个有效的解决方案。
给出了数组a=[a1,a2,…,an](1≤ai≤n)。如果存在一对索引l和r(1≤li=al+al+1+…+ar,则其元素ai称为特殊元素。换句话说,如果一个元素可以表示为数组中两个或多个连续元素的和(无论它们是否特殊),则称为特殊元素ai
打印给定数组a的特殊元素数。
例如,如果n=9,a=[3,1,4,1,5,9,2,6,5],那么答案是5:
· a3=4是一个特殊的元素, 因为a3=4=a1+a2=3+1;
· a5=5是一个特殊的元素, 因为a5=5=a2+a3=1+4;
· a6=9是一个特殊的元素, 因为a6=9=a1+a2+a3+a4=3+1+4+1;
· a8=6是一个特殊的元素, 因为a8=6=a2+a3+a4=1+4+1;
· a9=5是一个特殊的元素, 因为a9=5=a2+a3=1+4.
请注意,数组a中的某些元素可能是相等的——如果有几个元素是相等的并且是特殊的,那么所有这些元素都应该计算在答案中。
Input
第一行包含整数t(1≤t≤1000)——输入中的测试用例数。接下来是t个测试用例。
每个测试用例分为两行。第一行包含整数n(1≤n≤8000)——数组a的长度。第二行包含整数a1、a2、…、an(1≤ai≤n)。
保证输入中所有测试用例的n值之和不超过8000.
Output
输出t——每个给定数组的特殊元素数。
Example
Input
5
9
3 1 4 1 5 9 2 6 5
3
1 1 2
5
1 1 1 1 1
8
8 7 6 5 4 3 2 1
1
1
Output
5
1
0
4
0

分析

此题考虑使用前缀和的思想。

代码

#include
#include
#include
using namespace std;

int T,N,mx,t,l,m,cnt;

int main()
{
	scanf("%d",&T);
	while(T--)	//有T个测试用例
	{
		int s[8001]={0},sum[8001]={0},a[8001]={0};//s[t]表示t出现几次,sum[t]表示从头到t之和 ,a[t]=1表示有区间和为t 
		//每次开始的时候都将它们赋值成0(这样上次的情况不会对这次有影响了)
		scanf("%d",&N);
		cnt=mx=0;
		for(int i=1;i<=N;i++)	//有N个数
		{
			scanf("%d",&t);
			mx=max(mx,t);	//mx储存已输入的数中最大的数
			s[t]++;	//更新t出现的次数
			sum[i]=sum[i-1]+t;	//前缀和,表示从头到i之和
		}
		l=2;
		while(l<N)
		{
			for(int i=l;i<=N;i++)
			{
				m=sum[i]-sum[i-l];
				if(m<=mx)	//m比mx小并且有区间和为m
					a[m]=1;
			}
			l++;
		}
		for(int i=2;i<=mx;i++)
		{
			if(a[i]==1)	//有区间和为i
				cnt+=s[i];
		}
		printf("%d\n",cnt);
	} 
}

P - Max Sum (HDU - 1003)

英文题*7:Given a sequence a[1],a[2],a[3]…a[n], your job is to calculate the max sum of a sub-sequence.

给定一个序列a[1]、a[2]、a[3]……a[n],你的工作就是计算一个子序列的最大和。例如,给定(6,-1,5,4,-7),此序列中的最大和为6+(-1)+5+4=14。
Input
输入的第一行包含一个整数T(1<=T<=20),表示测试用例的数量。接下来是T行,每行以数字N开头(1<=N<=100000),然后是N个整数(所有整数都在-1000到1000之间)。
Output
对于每个测试用例,您应该输出两行。第一行是“Case #:”,#表示测试用例的编号。第二行包含三个整数,序列中的最大和,子序列的开始位置,子序列的结束位置。如果有多个结果,则输出第一个结果。在两个案例之间输出一个空行。
Sample Input
2
5 6 -1 5 4 -7
7 0 6 -1 1 -6 7 -5
Sample Output
Case 1:
14 1 4
(中间有个空)
Case 2:
7 1 6

#include
#include
using namespace std;

int arr[100001];
int T,N,cnt;
int l,r,ans,a,b,sum;

int main()
{
	scanf("%d",&T);
	cnt=1;
	while(T--)	//有T个案例
	{
		scanf("%d",&N);
		l=r=1,sum=-2000,ans=0;	//l,r分别代表现在子序列的左端点和右端点
		for(int i=1;i<=N;i++)	//有N个整数
		{
			scanf("%d",&arr[i]);	//存储输入的整数
			ans+=arr[i];	//存储现在子序列的和
			if(ans>sum)	//如果现在子序列的和大于之前子序列的最大和
			{	//记录现在子序列的信息
				a=l;	//a代表子序列的最大和的左端点
				b=r;	//b代表子序列的最大和的右端点
				sum=ans;	//sum代表子序列的最大和
			}
			if(ans<0)	//负数+整数/负数都只会更小
			{	//更新端点
				l=i+1;
				ans=0;
			}
			r=i+1;
		}
		printf("Case %d:\n%d %d %d\n",cnt++,sum,a,b);
		if(T!=0)	printf("\n");	//如果不是最后一个案例,就要空一行
	} 
}

你可能感兴趣的:(题解)