LeetCode第 336 题:回文对(C++)

336. 回文对 - 力扣(LeetCode)

暴力法

困难题,第一下还真没啥好的思路,直接暴力遍历再判断回文应该是不行的,复杂度太高。还是先写下来:

class Solution {
public:
    vector<vector<int>> res;
    bool isOk(const string &s1, const string &s2){
        string s = s1 + s2;
        int i = 0, j = s.size()-1;
        while(i < j){
            if(s[i] != s[j])    return false;
            ++i;--j;
        }
        return true;
    }
    vector<vector<int>> palindromePairs(vector<string>& words) {
        for(int i = 0; i < words.size(); ++i){
            for(int j = i+1; j < words.size(); ++j){
                bool flag = isOk(words[i], words[j]);
                if(flag)    res.push_back({i,j});
                if(flag && words[i].size() == words[j].size() || isOk(words[j], words[i]))
                        res.push_back({j, i});
            }
        }
        return res;
    }
};

emmm…很简单很直接也很拉跨,直接就超时了,不过也在意料之中。

哈希表

遇事不决先瞅瞅标签,字典树,哈希表。。。

先分析一下规律:

  • 两个单词a, b能够组成回文对,且长度相同,那么(a, b),(b, a)都可以。
  • 难点在于长度不同的时候,比如"lls", 和"sssll"这样的,能够组成回文对的条件是:短的那个是长的那个的逆序后缀(就是长的那个先翻转(“llsss”)过来,然后短的这个是长的那个的前缀(llsss)),然后长的这个除去那部分前缀的剩下部分(llsss)还得是回文串才行。

第二点总结就是:较长的单词逆序后,分割成两部分,一部分得和较短的单词相同,另一部分得是回文串。这两部分谁都可能在前面。

在这里插入图片描述
C++、字典树,注释详细 - 回文对 - 力扣(LeetCode):这个的图很形象

代码可以参考:[前缀树]leetcode336:回文对(hard)_algsup-CSDN博客_前缀树 回文

class Solution {
public:
    vector<vector<int>> res;
    unordered_map<string, int> map;
    set<int> set;//用来保存长度,有序
    bool isOk(string word, int left, int right)
    {
        while(left<right)
        {
            if(word[left++]!=word[right--])
                return false;
        }
        return true;
    }
    vector<vector<int>> palindromePairs(vector<string>& words) {
        for(int i = 0; i < words.size(); ++i){//建立map和set
            map[words[i]] = i;
            set.insert(words[i].size());
        }

        for(int i = 0; i < words.size(); ++i){
            auto word = words[i];
            auto size = word.size();
            reverse(word.begin(), word.end());//翻转
            //长度相等时,直接翻转后查找就行(注意排除叠词比如"bbb"翻转后还是"bbb"的情况)
            if(map.count(word) != 0 && map[word] != i)  res.push_back({i, map[word]});

            //除了上面的情况,还要判断所有比words短的单词,是否可以和word构成回文对
            auto up_bound = set.find(size);//bound是word长度上界
            for(auto it = set.begin(); it != up_bound; ++it){
                auto len = *it;
                if(isOk(word, 0, size-len-1) && map.count(word.substr(size-len)))
                    res.push_back({i, map[word.substr(size-len)]});

                if(isOk(word, len, size-1) && map.count(word.substr(0, len)) )
                    res.push_back({map[word.substr(0, len)], i});
            }
        }
        return res;
    }
};

代码逻辑其实很简单,对于每一个单词,我们先考虑该单词翻转后的单词是不是也在哈希表里面,是的话得到一组长度相同的匹配。然后,我们还要考虑该单词能不能和比它短的那些单词构成回文串,判断方法就是上面说的切成两部分判断。

可以看到该方法其实还是有点暴力的,不过使用了哈希表提升了效率。

字典树

字典树的实现就不用说了,这儿可以将单词逆序插入字典树,这样后面检索的时候就不用进行翻转了,可以节省一小点时间。因为插入字典树是逐个字符插入的,正序插、逆序插并没有区别。

代码重点还是单词切割成两部分判断,其他的地方采用什么数据结构只是为了检索或者判断算法服务的。

class Trie{
public:
    struct TrieNode{
        int ID = -1;//该单词在原words中的下标
        TrieNode* next[26] = {NULL};
    };
    Trie() : root(new TrieNode){}
    void insert(const string &word, int index){
        auto p = root;
        for(int i = word.size()-1; i >= 0; --i){//这儿逆序插入
            int idx = word[i] - 'a';
            if(p->next[idx] == NULL)    p->next[idx] = new TrieNode;
            p = p->next[idx];
        }
        p->ID = index;
    }
    int search(const string &word, int l, int r){
        auto p = root;
        for(int i = l; i <= r; ++i){
            int idx = word[i] - 'a';
            if(p->next[idx] == NULL) return -1;//没找到
            p = p->next[idx];
        }
        return p->ID;//返回该单词在原words中的下标
    }
private:
    TrieNode* root; 
};

class Solution {
public:
    vector<vector<int>> res;
    vector<vector<int>> palindromePairs(vector<string>& words) {
        int n = words.size();
        auto trieTree = new Trie();//构建前缀树并(逆序)插入单词
        for(int i = 0; i < n; ++i)  trieTree->insert(words[i], i);

        for(int i = 0; i < n; ++i){//对于words中每个单词
            int m = words[i].size();
            //开始取word的子串进行判断
            //idx != i是因为 "aaa"这样的字符串查询结果就是idx == i的,两者指向同一字符串
            for(int j = 0; j < m+1; ++j){
                if(isOk(words[i], j, m-1)){//截取的后一部分是回文
                    int idx = trieTree->search(words[i], 0, j-1);
                    if(idx != -1 && idx != i)   res.push_back({i, idx});
                }
                if(j > 0 && isOk(words[i], 0, j-1)){//截取的前一部分是回文
                    int idx = trieTree->search(words[i], j, m-1);
                    if(idx != -1 && idx != i)   res.push_back({idx, i});
                }
            }
        }
        return res;
    }
private:
    bool isOk(const string& s, int l, int r){//判断是否是回文
        while(l < r){
            if(s[l] != s[r])    return false;
            ++l;--r;
        }
        return true;
    }
};

代码虽然看起来有点长,但是主要是字典树的实现代码比较多(但是并不难)。不过上述的代码还有优化的空间,因为单词切割那儿上述代码每种可能的方式都进行了尝试。这样其实是没有必要的(参考哈希表方法里面的处理),因为某些切割毫无意义,原words里根本就没有这个长度的单词。

所以可以再加一个set来保存单词长度。

猜你喜欢

转载自blog.csdn.net/qq_32523711/article/details/107837545