Phone List (HDU - 1671)

版权声明:转载请注明 https://blog.csdn.net/li13168690086/article/details/81381173

题目描述

  Given a list of phone numbers, determine if it is consistent in the sense that no number is the prefix of another. Let’s say the phone catalogue listed these numbers: 
  1. Emergency 911 
  2. Alice 97 625 999 
  3. Bob 91 12 54 26 
  In this case, it’s not possible to call Bob, because the central would direct your call to the emergency line as soon as you had dialled the first three digits of Bob’s phone number. So this list would not be consistent. 

Input

  The first line of input gives a single integer, 1 <= t <= 40, the number of test cases. Each test case starts with n, the number of phone numbers, on a separate line, 1 <= n <= 10000. Then follows n lines with one unique phone number on each line. A phone number is a sequence of at most ten digits.

Output

  For each test case, output “YES” if the list is consistent, or “NO” otherwise.

Sample Input

2
3
911
97625999
91125426
5
113
12340
123440
12345
98346

Sample Output

NO
YES

题目分析

  问题致力于判断是否所有电话能够都拨打成功。首先拨通规则是这样描述的:给出911、97625999、91125426等三个号码;在拨打911时,与其他号码不冲突,可以拨通;在拨打97625999时,与其他号码不冲突、可以拨通;但在拨打91125426时,当按下前三个数字后,拨打的是911的电话(神经病,自动拨号的吗?),所以产生了冲突,也就被认为不能拨通,最后答案输出"NO"(只要有1个号码不能拨通,就认为失败)。反之,如果所有号码,都不产生冲突,那就成功。要注意的是,它冲突的号码,在其他号码里一定是连续的,若是分开就不算。举个例子:有三个号码12340,5123405,123440;那么“12340”和“5123405”就产生冲突,而“12340”和“123440”不产生冲突,因为“12340”在“123440”中不是连续的数字。

解题思路

  通过分析,我们知道,只要判断自身任意一段连续的数字,是否存在于其他数字当中,就可以找出哪些是冲突的号码了。细想之下,这个想法就是字符串匹配的问题。我们知道字符串匹配有很多种解法,而在这里,我将用其中一种解法来讲述---字典树。若想要关注其他解法,请另寻他路,谢谢(其实我就是刷专题,这次轮到字典树的专题了,所以才用字典树来解,理解理解)。

  关于字典树,我们有两种思路:

  1. 先将所有电话号码,存入字典树中,然后再用搜索来判断,当前字符串中直到最后一个字符之前的结点是否有标记,如果有就是产生了冲突 (有标记就代表,有其他字符串走过一样的路,使得路线重叠,即前面出现了相同的字符串),这种做法,可行,也能AC。就是花的时间我觉得有点多,因为要先建立好字典树,才能进行搜索。

  2. 如果我们边建立树,边判断搜索,会不会更快点呢? 首先我们可以在读入号码的时候,就建立树;或者先读完,再建树也差不了多少,关键在于边建树边判断。即在insert函数里做修改,可以完全不用search函数。其实我们只要再设置一个变量即可,新建一个newnode变量,意思是如果在插入的过程中产生了新的结点(newnode加1),就认为当前字符串是一个新字符串,与之前插入的字符串不冲突;再加上思路1中的判断,如果前在最后结点之前出现了标记,那么还是冲突。

  思路1和思路2不同点在于,newnode思想。如果当前插入的字符串,没有出现最后结点前有标记,也产生了新的结点 (即新路),那就认为它是与众不同的,没有冲突的。反之就与前面的号码有冲突。

  对了!还有一个易错点,就是空间问题。因为字典树占有空间量很大,如果数据量一多(就像本题),就会爆栈,所以要写多一个销毁树的方法,具体见下面代码。

  关于字典树的理解请移步字典树的初步理解

AC代码

  思路1

#include <iostream>
#include <string>
#include <string.h>
#include <vector>
using namespace std;

#define ALPHABET_SIZE 10
vector<string> keys;

typedef struct trie_node
{
    int count;   // 记录该节点代表的单词的个数
    trie_node *children[ALPHABET_SIZE]; // 各个子节点
}*trie;

trie_node* create_trie_node()
{
    trie_node* pNode = new trie_node();
    pNode->count = 0;
    for(int i=0; i<ALPHABET_SIZE; ++i)
        pNode->children[i] = NULL;
    return pNode;
}

void trie_insert(trie root,const char* key)  //结点插入函数
{
    trie_node* node = root;
    const char* p = key;
    while(*p)
    {
        if(node->children[*p-'0'] == NULL)
        {
            node->children[*p-'0'] = create_trie_node();
        }
        node = node->children[*p-'0'];
        ++p;
    }
    node->count += 1;
}

/**
 * 查询:不存在返回0,存在返回出现的次数
 */
int trie_search(trie root,const char* key)  //搜索匹配函数
{
    trie_node* node = root;
    const char* p = key;
    int ans = 0;    //新建了个变量ans,用来记录在最后结点之前的结点,是否出现了标记
    while(*p && node!=NULL)
    {
        node = node->children[*p-'0'];
        ++p;
        if(*p && node!=NULL && node->count >= 1){    //如果结点不是最后一个,并且有标记,那就记录并退出搜索。
            ans = 1;
            break;
        }
    }
    if(ans)
        return 1;
    else
        return 0;
}

int deltree(trie T)    //销毁字典树,很重要,不然空间大爆炸,boom!A不了C啊。
{
    int i;
    if(T==NULL)    //如果结点为NULL即已清空,直接退出
        return 0;
    for(i=0;i<10;i++)    //每个结点都占有10个空间,这具体是实际来定,因为这道题全部都是数字,所以10就足够了
    {
        if(T->children[i]!=NULL)    //如果结点的下一个结点不为空,就继续递归
            deltree(T->children[i]);
    }
    free(T);    //释放当前结点空间
    return 0;
}

int main()
{
    int n,t;
    cin >> n;    //读入测试总数
    while(n--){
        cin >> t;    //读入号码数量
        keys.clear();
        for(int i = 0; i < t; i++){
            string a;    
            cin >> a;
            keys.push_back(a);    //这里比较蠢,用了一个vector,最初没有销毁树,以为优化下存储号码的方式,就不会空间爆炸,结果没用,其实直接用char数组一样ok(可以参考思路2)。
        }
        trie root = create_trie_node();    //建立根源结点,即建立新树
        for(int i = 0; i < t; i++){
            const char *p = keys[i].data();    //将string转换成const char*,为了适应插入函数,比较蠢,不改了,作为警示吧,好一点的请参考思路2
            trie_insert(root, p);    //插入结点
        }
        int k = 0;
        for(int i = 0; i < t; i++){    //这里才到真正的判断搜索
            const char *p = keys[i].data();    //蠢想法
            k = trie_search(root, p);    //搜索字符串
            if(k == 1){    //如果返回值为1,就是有冲突,输出"NO",并退出
                cout << "NO" << endl;
                break;
            }
        }
        if(k == 0)
            cout << "YES" << endl;
        deltree(root);    //销毁树,真正减少空间的方法,而不是用我的蠢想法
    }
    return 0;
}

  思路2

#include <iostream>
#include <string>
#include <stdio.h>
#include <string.h>
#include <vector>
using namespace std;

#define ALPHABET_SIZE 10
char keys[10010][11];
int flag;

typedef struct trie_node
{
    int count;   // 记录该节点代表的单词的个数
    trie_node *children[ALPHABET_SIZE]; // 各个子节点
}*trie;

trie_node* create_trie_node()
{
    trie_node* pNode = new trie_node();
    pNode->count = 0;
    for(int i=0; i<ALPHABET_SIZE; ++i)
        pNode->children[i] = NULL;
    return pNode;
}

void trie_insert(trie root,const char* key)    //结点插入函数
{
    trie_node* node = root;
    const char* p = key;
    int newnode = 0;    //新建一个newnode变量,作为有没有产生新路的标记
    while(*p)
    {
        if(node->count >= 1)    //思路1中的判断,在最后结点前的结点是否有标记
            flag = 1;
        if(node->children[*p-'0'] == NULL)    //是否产生新结点
        {
            newnode++;    //做标记
            node->children[*p-'0'] = create_trie_node();
        }
        node = node->children[*p-'0'];
        ++p;
    }
    if(!newnode)    //如果没有新路,flag为1,即有冲突。
        flag = 1;
    node->count += 1;
}

int trie_search(trie root,const char* key)    //虽然没用到,还是留着呗
{
    trie_node* node = root;
    const char* p = key;
    int ans = 0;
    while(*p && node!=NULL)
    {
        node = node->children[*p-'0'];
        ++p;
        if(*p && node!=NULL && node->count >= 1){
            ans = 1;
            break;
        }
    }
    if(ans)
        return 1;
    else
        return 0;
}

int deltree(trie T)    //这个销毁树的函数挺重要,因为字典树本身结构占有空间量大,所以每用完一次,就要销毁,不然会出现超空间的错误,真的!
{
    int i;
    if(T==NULL)
        return 0;
    for(i=0;i<10;i++)
    {
        if(T->children[i]!=NULL)
            deltree(T->children[i]);
    }
    free(T);
    return 0;
}

int main()
{
    int n,t;
    cin >> n;    //读入测试总量
    while(n--){
        cin >> t;    //读入号码个数
        flag = 0;    //冲突表示默认为0
        for(int i = 0; i < t; i++){
            scanf("%s",keys[i]);    //读入号码,这里我采用先读完,再建树,其实也可以边读边建,如果遇到flag为1时,就不需要存进keys里,这样也会少一点空间,但其实差别不大
        }
        trie root = create_trie_node();    //建立新根源结点,即建立新树
        for(int i = 0; i < t; i++){
            trie_insert(root, keys[i]);    //将号码插入字典树,并判断是否冲突
            if(flag)    //如果冲突,就不需要再构造字典树了,直接退出
                break;
        }
        if(flag == 0)    //如果冲突标志没变化,就成功,反之失败
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
        deltree(root);    //用完一棵,砍一棵,环保一点,省空间,不然爆栈
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/li13168690086/article/details/81381173