寒假集训总结(一):关于快速匹配
寒假进行了数日的集训,感觉收获颇丰,虽然中间由于生病耽误了几天的训练,但后期又跟进进行了补充,仍然获得了许多宝贵的经验以及认识到了自身的不足,这一次先进行一种常见题型的总结,并列出两道典型例题,给出一些个人见解,不保证为最优解法,但至少为AC代码,如有不足,还望指出。
例题一:7-14 QQ帐户的申请与登陆(25 分)
7-15 QQ帐户的申请与登陆(25 分)
实现QQ新帐户申请和老帐户登陆的简化版功能。最大挑战是:据说现在的QQ号码已经有10位数了。
输入格式:
输入首先给出一个正整数N(≤10
5
),随后给出N行指令。每行指令的格式为:“命令符(空格)QQ号码(空格)密码”。其中命令符为“N”(代表New)时表示要新申请一个QQ号,后面是新帐户的号码和密码;命令符为“L”(代表Login)时表示是老帐户登陆,后面是登陆信息。QQ号码为一个不超过10位、但大于1000(据说QQ老总的号码是1001)的整数。密码为不小于6位、不超过16位、且不包含空格的字符串。
输出格式:
针对每条指令,给出相应的信息:
1)若新申请帐户成功,则输出“New: OK”;
2)若新申请的号码已经存在,则输出“ERROR: Exist”;
3)若老帐户登陆成功,则输出“Login: OK”;
4)若老帐户QQ号码不存在,则输出“ERROR: Not Exist”;
5)若老帐户密码错误,则输出“ERROR: Wrong PW”。
输入样例:
5
L 1234567890 myQQ@qq.com
N 1234567890 myQQ@qq.com
N 1234567890 myQQ@qq.com
L 1234567890 myQQ@qq
L 1234567890 myQQ@qq.com
输出样例:
ERROR: Not Exist
New: OK
ERROR: Exist
ERROR: Wrong PW
Login: OK
原题地址:https://pintia.cn/problem-sets/15/problems/723
一道典型的数据结构题,主要考察快速查找以及重复的判断,难度一般,此题个人有两种解法,第一种是建立哈希表,可以用C语言实现。笔者通过阅读他人的解法受到了启发,给出原博地址,https://www.cnblogs.com/joeylee97/p/6628506.html;
第二种是利用C++STL中的map,较为简便,下面先给出C语言哈希表的实现:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef long long Datatype_account; //由于账户可能大于int型的最大值,因此定义为LL型
typedef char Datatype_password; //定义char型变量来标识密码
typedef struct List List; //声明链表的节点类型
typedef struct Hashlist Hashlist; //声明哈希表的节点类型
struct List //定义链表
{
Datatype_account id;
Datatype_password pa[20];
List *next;
};
struct Hashlist //定义哈希表
{
int size;
List *table;
};
Hashlist *creat(int n); //定义一个创建哈希表的函数,返回值为哈希表的首地址
int nextprime(int n); //采用除留余数法,因此需要找到大于总数据规模的最小素数
int Hash(Hashlist *H,Datatype_account key); //定义散列函数
List *find(Hashlist *H,Datatype_account key);//定义查找函数
void Login(Hashlist *H,Datatype_account key,Datatype_password *p);//定义登录函数
void Apply(Hashlist *H,Datatype_account key,Datatype_password *p);//定义申请函数
Hashlist *creat(int n)
{
Hashlist *H = (Hashlist*)malloc(sizeof(Hashlist)); //首先为散列表首地址分配空间
H -> size = nextprime(n); //利用除留余数法,得到散列表的大小
int i = 0;
H -> table = (List*)malloc(H -> size * sizeof(List));//为散列表分配空间
for(i = 0;i < H -> size;i++) //散列表的初始化
{
H -> table[i].next = NULL;
H -> table[i].id = 0;
H -> table[i].pa[0] = ' ';
}
return H;
}
int nextprime(int n) //寻找素数
{
int i,flag;
while(1)
{
flag = 1;
for(i = 2;i < n;i++)
{
if(n % i == 0)
{
flag = 0;
}
}
if(flag)
{
break;
}
else
{
n++;
}
}
return n;
}
int Hash(Hashlist *H,Datatype_account key)
{
long long index = key % H -> size; //直接使用除留余数法得到散列函数对应的函数值
return index;
}
List *find(Hashlist *H,Datatype_account key)
{
long long index = Hash(H,key); //得到键值对应的函数值
List *p = H -> table[index].next; //将函数值映射到散列表中
while(p && key != p -> id) //若该节点存在且键值不等于列表中的值,则继续向下查找
{
p = p -> next;
}
return p; //返回该节点
}
void Login(Hashlist *H,Datatype_account key,Datatype_password *p)
{
List *f = find(H,key); //定义一个List型节点并按键值查找
if(f && !strcmp(f -> pa,p)) //若该节点存在且密码相符则输出登录成功
{
printf("Login: OK");
}
else if(f && strcmp(f -> pa,p)) //若节点存在但密码不符
{
printf("ERROR: Wrong PW");
}
else if(!f) //若节点不存在
{
printf("ERROR: Not Exist");
}
}
void Apply(Hashlist *H,Datatype_account key,Datatype_password *p)
{
List *f = find(H,key); //仍然是先进行查找
if(f) //若节点已存在,则提示错误信息
{
printf("ERROR: Exist");
}
else //若不存在,则建立节点,加入到散列中
{
f = (List*)malloc(sizeof(List));
long long index = Hash(H,key);
f -> next = H -> table[index].next;
H -> table[index].next = f;
f -> id = key;
strcpy(f -> pa,p);
printf("New: OK");
}
}
int main(void)
{
int n = 0;
Datatype_account acc;
Datatype_password pa[20];
char choose = ' ';
scanf("%d",&n);
Hashlist *H = creat(n); //预先分配空间
while(n--)
{
getchar();
scanf("%c%lld%s",&choose,&acc,&pa);
if(choose == 'L')
{
Login(H,acc,pa);
}
else
{
Apply(H,acc,pa);
}
printf("
");
}
return 0;
}
下面是C++STl map的实现;
#include<bits/stdc++.h>
using namespace std;
map<string,string>q; //构造一个map类,为了简单处理,笔者将容器内的存储对象定义为两个string类
void login(string a,string p); //定义一个登陆函数
void apply(string a,string p); //定义一个申请函数
void login(string a,string p)
{
map<string,string> :: iterator s;//定义一个对应两个string类对象的map迭代器
s = q.find(a); //对map进行查找
if(s != q.end() && p == s -> second) //若找到且密码相等,提示登陆成功
{
printf("Login: OK");
}
else if(s != q.end() && p != s -> second) //若存在但密码不等,提示失败
{
printf("ERROR: Wrong PW");
}
else if(s == q.end()) //若查找失败,则提示不存在
{
printf("ERROR: Not Exist");
}
}
void apply(string a,string p)
{
map<string,string> :: iterator s;
s = q.find(a);
if(s != q.end()) //若查找成功,则提示账户已存在
{
printf("ERROR: Exist");
}
else //若查找失败,则将该账户作为新元素插入map中
{
q.insert(pair<string,string>(a,p));
printf("New: OK");
}
}
int main(void)
{
int i = 0,n = 0;
string account,password,temp,temp2,choose;
scanf("%d",&n);
for(i = 0;i < n;i++)
{
cin >> choose;
cin >> temp;
cin >> temp2;
if(choose == "L")
{
login(temp,temp2);
}
else
{
apply(temp,temp2);
}
if(i != n - 1)
{
printf("
");
}
}
return 0;
}
例题二:7-16 航空公司VIP客户查询(25 分)
不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
输入格式:
输入首先给出两个正整数N(≤10
5
)和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:18位身份证号码(空格)飞行里程。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤10
5
),随后给出M行查询人的身份证号码。
输出格式:
对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出No Info。每个查询结果占一行。
输入样例:
4 500
330106199010080419 499
110108198403100012 15000
120104195510156021 800
330106199010080419 1
4
120104195510156021
110108198403100012
330106199010080419
33010619901008041x
输出样例:
800
15000
1000
No Info
原题链接: https://pintia.cn/problem-sets/959995131537092608/problems/959995183282221063
同样是一道考察快速查找匹配的题目,笔者开始仍然是使用哈希表的方法做,单较为繁琐,后来改为利用Set容器,但有一些小问题,之后再讨论,至于使用哈希表的做法,参照上题,稍作修改以及对散列函数重新分析,可以得到相同的效果,有兴趣可以自行尝试。
二:对于该类题型的分析:
我们以7-16为例,可以稍微进行一些散列表的分析,对于查找与匹配等问题,我们需要保证两点,其一是其高效性,其二是其准确性,对于一般的问题,我们往往会采取开数组的方法来处理,而我们又知道,最快捷的访问方法是利用数组的随机访问方式,但可惜的是,我们对于绝大多数数据,无法使其得到单一的与数组下标对应的值,例如7-16,身份证号为18位,若其为纯数字的话,或许有些高级语言原生支持大数存储,那么数组呢,开10^18次方的吗,显然不现实,何况校验码还为字母,这时我们就想在保证其准确性与效率的前提下,使其得到近似于随机访问的O(1)的效率,这时我们就需要使用哈希算法,将其关键字进行离散化,得到固定长度的函数值,并映射到对应的位置上,从而得到较高的效率。举个简单的例子,假设我们要在全校范围内寻找到高为一米八的人,我们可以采取如下方式,带着全校的人名单,一一查找(顺序查找);第二,使全校同学按身高排好队(队头最高),并从队伍中间开始查找,若该同学高于一米八,则向队伍后面查找,若低于一米八,则向 队伍前面查找(二分查找),三,我们预先知道了一米八以上的人占全校人数的三分之一,那么我们直接走向队伍距离队首三分之一处,开始查找(插值搜索),或许这次我找到了一米八的人,但是下次我要找一米六的人呢,再重新排好队(排序),再进行查找吗,显然太费时间了,(何况你的学生们还会有意见),这时我们不妨再开学时来一次统计,例如,一米八到两米的同学的名字放在一张单子上,一米六到一米八的同学的名字放在一个单子上,以此类推,那么下次查找时,我直接拿来对应身高的单子,去查找即可,大大节约了时间。(例子不太恰当,实际上入学体检时已经记录好了,何况学生是会说话的),那么我们将数据进行散列处理的目的就在此,将一系列数据按照预先设定好的函数进行处理,得到固定长度的函数值,(按身高区间放入名单),那么我下次进行查找时,直接将关键字的值放入散列函数处理得到对应的值,并在该区间内查找(在名单内查找),这样可以将搜索范围大大缩小。
但是随之而来的是第二个问题:冲突
即关键字的值集中在某一较小区间内,(依据散列函数而定),有可能使不同的键值得到相同的哈希值(例如体校,可能百分之九十的人都身高一米八),这时候会导致查找时可能找到不正确的元素,例如7-16,我们从实际入手,身份证号的意义为1,2位为省,自治区直辖市代码,3,4位为地级市,5,6位为县级代码,714位为出生年月日,1517位为顺序号,最后一位为校验码,那么我们可以得出如下结论(数学证明部分略去不计,数字分析法),可以发现若人员集中在某一地区,或出生年月为某一时间段内,会导致1~14位的数字分布不均匀,较容易产生冲突,这时我们就容易联想到最后几位顺序码,该字段的冲突概率最低,那么我们不妨就取身份证号的最后数位作为键值,并进行散列处理,当然这只是其中一种方式,也是最简单的方式,你仍可以在身份证号中任取数位作为键值(前提是不相邻的数位),依实际情况而定。
实际上在散列中,冲突是大概率事件,那么较为直接有效的方式就是拉链法,即我们得到相同函数值时,不直接将其存储在对应地址,而是在该地址的基础上,向下延伸(用链式存储),这样我们就解决了不同元素抢占地址的问题,在查找时,我们先得到该地址(缩小范围),再向下查找,直到得到要查找的值(或未找到),当然这种方式也有弊端,即堆积问题,若大多数数据的函数值都集中在该地址上,则其查找效率有可能退化为顺序查找的效率,这时我们可以考虑使用哈希树,在此就不过多展开,拉链法对于这两道例题已经足够。
对于散列的讨论就先到这里,其他的一些常用的查找方式刚才也提到过,例如二分查找,插值搜索等,这些方法会在有较好的例题时拿来分析,关于方法的选择,并没有最优,只有相对最优(主要还是依据数据规模以及类型而定),这次先到这里,若有问题,还望指正。