[G] The Roll【链接】
这道题呢,由于是本人出的,所以要好好讲一讲了~
先说一下背景:在本题中,每组数有n个班级,每个班级每个同学的学号依次是1~n号,从这句话可知,一班的一号与二班的一号有着同样的学号。
然后是题意:一个人去点名,每次点n个班级,每个班级记录下来缺课的学号,这样就有了一个各班缺课人数以及学号的表格了,你的任务是统计表中每个学号的逃课次数和另一个数据(所有班级人数 - id数量(不含重复))。举例说明:一班4人,两个人不在,1和3号;二班5人,3个人不在,1号4号5号;你的统计表应该是这样的:两次的有:1号;一次的有3号4号5号;最后一个数据是4 + 5 - 4 = 5。说了这么多,我再问一个问题:是不是每个班级开一个数组?答案当然是否定的,因为班级的id重复着,统计的id也是重复着的。
解法:因为一个班级最多有100人,我先开一个105的数组,然后找出n个班级中,最多人数的一个班级,赋值给max变量;然后我每次操作都在1~max之间,包括排序也只在这一段上排序,这样效率就挺好的了。拿刚才的举例子(设数组是num[105]):报一个学号(id),我就执行:num[id] ++;然后按照num[id]的值从大到小排个序,答案就出来了。可是这里就有个问题了,我要是开一个一维数组,排序以后学号和次数就对不上了,“0号”永远都是次数最多的那个人 o(>﹏<)o
所以我们需要一个二维数组,然后考虑如何排序,对于一维数组以外的排序,我们一般用qsort来实现,这就是整个程序的重点,不过如果不会这个也没关系,我的数据挺弱的,用其他方法只要能实现,一样能A掉不会超时。qsort的话,建议大家好好看一看,这个函数是相当有用的一个东西,功能比sort强大得多,具体用法问度娘吧~。继续说题,按照刚才的描述,开一个num[105][2]的数组,如下图上的半部分,这个时候如果能把每一列当做一个整体的话,那么接下来只要像sort一样排一下,答案就出来了。
看起来很复杂,但是用qsort的话,你只要知道两件事情就行了,其他的就不用担心了,其一就跟sort一样,你要知道排序的范围;其二是怎么排序(因为每一列有两个东西),这里的话要先按照num从大到小排序,num值相同的话按照id从小到大排序。
程序中用到其一的语句在这:
qsort(n+1,max,sizeof(n[0]),cmp);
int cmp( const void *a , const void *b )
{
struct info *c = (info *)a;
struct info *d = (info *)b;
if(d->time != c->time)
return d->time - c->time;
else
return c->id - d->id;
}
剩下来就是统计缺课的id数量来输出最后一个数据。这个的话排序完以后输出每一个数据的同时累加个数,最后总数减去这个数就行了。
有一点要注意的是格式:
2次是2 times:
1次是1 time:而不是1 times:
最后再加一个判断:
如果没人逃课,输出"Nobody Skipped The Classes!"
整个程序如下:
#include<iostream> #include<algorithm> #include<cstdio> using namespace std; struct info { int id; int time; }n[105]; int cmp( const void *a , const void *b ) { struct info *c = (info *)a; struct info *d = (info *)b; if(d->time != c->time) return d->time - c->time; else return c->id - d->id; } int main() { //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); int tot,num,id; int max = 0,sum; while(~scanf("%d",&tot)) { sum = 0; memset(n,0,sizeof(n)); for(int i = 0 ; i < tot ;i ++) { scanf("%d",&num); sum += num; max = max<num?num:max; scanf("%d",&num); for(int j = 0 ; j < num ; j ++) { scanf("%d",&id); n[id].time ++; } } for(int i = 1 ; i <= max; i ++) n[i].id = i; qsort(n+1,max,sizeof(n[0]),cmp); int it = 1,tmp = n[1].time; while(tmp > 0 && it <= max) { if(tmp >1)printf("%d times:",tmp); else printf("%d time :",tmp); while(n[it+1].time == tmp) { if(n[it].id != 0)printf("%d ",n[it].id); it ++; } printf("%d\n",n[it].id); it ++; tmp = n[it].time; } if(it == 1)printf("Nobody Skipped The Classes!\n"); printf("good students:%d\n",sum-it+1); } return 0; }
[H] Where is XiSi【链接】
题意:N个学生对应N句话,比如说有三个学生,原本是:
学生1 - 语句1 学生2 - 语句2 学生3 - 语句3
现在问你每个人都对不上自己语句共有多少种情况,例如:
学生1 - 语句2 学生2 - 语句3 学生3 - 语句1
这就是完全组合错误的其中一种情况,数学上把这个叫做“错排”,我可以再问一个问题:“十本不同的书放在书架上。现重新摆放,使每本书都不在原来放的位置。有几种摆法?” 这也是一种用到错排例子。这个问题推广抽象一下,就是错排问题: n个有序的元素应有n!种不同的排列。如若一个排列式的所有的元素都不在原来的位置上,则称这个排列为错排。
经过各种努力,得到如下错排公式:
特殊地,M⑴=0,M⑵=1M(n)=( n-1)[ M( n-2)+M( n-1)]
现在看来,这就是一道简单的数学题了,只要输入一个数,然后用公式计算一下,最后输出就行了,但是如果做过NOJ一起来数兔子这道题并且对打表有概念的话,就会知道这是种很不是办法的办法——效率实在是不怎么样,正确的做法是:
①:观察公式,嗯,已经够浓缩的了,再精辟估计很难了,就这样好了。
②:我把所有结果列出来。你得先看看这样现不现实(要是你列出所有答案得花5个小时,而程序要1秒内......)。看到这句话放心了【Each test contains one integer N(2<=N<=12).】,才12组数据,打表的话没有任何压力,秒杀了。
③:用上刚才的计算结果,问一个问题,去找相对应的答案,直接输出。
这一段话解释一下打表这个东西,会了的童鞋直接跳过:首先明确一个概念,输出一个数组中的任意元素是不需要占用什么时间的,因为我写上a[5]的话a[5]就马上出来了,因为不需要从a[0]去找a[1]再找a[2]直到a[5]。然后我假设从前一个答案得到后一个答案需要2ms,输出需要0ms。现在套用本题:
计算a[5]:
要先计算a[2] -> a[3] -> a[4] -> a[5] 即 2ms + 2ms + 2ms + 2ms = 8ms;然后输出;
计算a[6]:
要先计算a[2] -> a[3] -> a[4] -> a[5] -> a[6] 即 2ms + 2ms + 2ms + 2ms + 2ms = 10ms;然后输出;
发现了吧,计算a[6]本不该浪费这么多时间的,前面都有a[5]了,就因为没有利用起来让你多浪费了10ms;
于是乎,打表就是先一口气算出所有的答案:
计算a[2] -> a[3] -> a[4] -> a[5] -> a[6] -> a[7] -> a[8] -> a[9] -> a[10] -> a[11] -> a[12] -> a[13]
花费时间共24ms,然后问一个输出一个,夸张的描述就是这样做在OJ上只会用掉24ms,有没有觉得像光速一样了呢~
通常我们多算一两个答案,这个道理就像为什么数组要开大一点点一样。
对于打表这种做法的态度:死月学长强烈不建议我们碰到这种事情就打表(如果打表能解决的话),这是个不好的习惯,ACM讲究的就是算法、效率、灵活、技巧这些东西,打表的话就不会去想诸如“如果能打表但是不让你打表怎么办” 之类的问题了,就起不到培养ACM水平的初衷了。希望童鞋们能记住我的三点建议:
①:对于平时的练习,能不打表就不要打表,如果不打表一直超时或者没有找到有效的办法,才去采用打表的办法, 当然,我们OJ或者其他OJ上的某些题目,就是让你在学习“打表法”的时候做的题,数据设计起来就是让你用打表的方法过的,其他基本没法过,所以有的时候不要死磕,打表有时候不代表弱,有的题目打表得绕着弯子也是挺有难度的,真要打表的话尽管打吧~
②:当你参加比赛的时候,就不要再想着学习了,你的目标只有三个,AC、AC,还是AC。只要能A掉,什么都不管,运动员才不会在比赛的时候想着练练球~ 学习的话是在比赛后再细细琢磨的事情了~
③:对于我们的OJ,允许打表,但是请不要忘记那份进取心~ 你要想着的是:打表太没水平了真是弱,这可不是我,一个兢兢业业的ACMer的风格~!
#include <cstdio> #include <iostream> using namespace std; int d[20]; int main() { int n; int p = 1; d[1] = 0; for (int i = 2; i < 13; i++) { d[i] = i * d[i - 1] + p; p = - p; } while (~scanf("%d", &n)) printf("%d\n", d[n]); return 0; }
[I] Shen【链接】
这道题的描述有点。。。咳咳咳。。。还是直接切入正题说题意吧:
一个英雄有好几种武器,每种武器数量相同,英雄每次使用一种武器中的一个武器,全部使用完以后问你,这个英雄最少有多少个武器(不是种)。意思很明确,假如我有3种武器,用的最多的那种武器用了3次,我就可以说武器至少有 9把,因为我每种武器至少有3把我才能使用3个同类武器。
所以问题转化成:
因为种类的上限是50种,所以有一种办法是开一个大小超过50的数组来处理问题,要是不用数组的话,有一个很好用的"map容器"可以用来处理此题,而且能够非常令人满意。prnitf("%d\n",种类数 * 用的最多的武器的数量);
因为现在的时间是:
#include<iostream> #include<map> using namespace std; map<int ,int > mp; int main() { int tot,num,max; while(~scanf("%d",&tot)) { mp.clear(); max = 0; while( tot --) { scanf("%d",&num); mp[num] ++; } map<int ,int >::iterator it = mp.begin(); for(; it !=mp.end(); it ++) { if(max < it->second) max = it->second; } printf("%d\n",max * mp.size()); } return 0; }