最长回文子串

这是算法的第5题,描述如下:

给定一个字符串s, 找到s中最长的回文子串。你可以假设s的最大长度为1000.

示例1:

输入:”babad”

输出:”bab”

注意:”aba”也是一个有效答案。

示例2:

输入:”cbbd”

输出:”bb”

此题难度:中等,通过率30%过一点。

题解转自官方解法,链接如下:

转自:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/

方法一:动态规划

思路与算法

对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道

“bab” 是回文串,那么“ababa” 一定是回文串,这是因为它的首尾两个字母都是 “a”。

根据这样的思路,我们就可以用动态规划的方法解决本题。我们用P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串:
P(i,j) = true if 如果子串 Si…Sj 是回文串;

P(i,j) = false 其他情况。

这里的「其它情况」包含两种可能性:
s[i,j] 本身不是一个回文串;
i>j,此时 s[i,j] 本身不合法。

那么我们就可以写出动态规划的状态转移方程:
P(i,j)=P(i+1,j−1)∧(S_i == S_j)

也就是说,只有 s[i+1:j−1] 是回文串,并且s 的第i 和 j个字母相同时,s[i:j] 才会是回文串。

上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1 或 2。对于长度为1 的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:

P(i,i)=true

P(i,i+1)=(Si==Si+1)

根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 P(i,j)=true 中 j−i+1(即子串长度)的最大值。注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。

复杂度分析

时间复杂度:
O(n^2),其中 n 是字符串的长度。动态规划的状态总数为 O(n^2),对于每个状态,我们需要转移的时间为 O(1)。

空间复杂度:
O(n^2),即存储动态规划状态需要的空间。

方法2:中心扩展算法

思路与算法

我们仔细观察一下方法一中的状态转移方程:

P(i,j) = true;

P(i, i+1) = (S_i == S_i+1);

P(i,j)=P(i+1,j−1)∧(S_i == S_j)

出其中的状态转移链:
P(i,j)←P(i+1,j−1)←P(i+2,j−2)←⋯←某一边界情况

可以发现,所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。

边界情况即为子串长度为 1 或 2 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从 P(i+1,j−1) 扩展到 P(i,j);如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。

聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。

复杂度分析

时间复杂度:O(n^2),其中 n 是字符串的长度。长度为 1 和 2 的回文中心分别有 n 和 n−1 个,每个回文中心最多会向外扩展 O(n) 次。

空间复杂度:O(1)。

代码如下(将两个题解放在一个文件里,分别为_s1和_s2):

def longestPalindrome_s1(s):
    n = len(s)
    dp = [[False] * n for _ in range(n)]
    ans = ""
    for l in range(n):
        for i in range(n):
            j = i + l
            if j >= len(s):
                break
            if l == 0:
                dp[i][j] = True
            elif l == 1:
                dp[i][j] = (s[i] == s[j])
            else:
                dp[i][j] = (dp[i+1][j-1] and s[i] == s[j])
            if dp[i][j] and l + 1 > len(ans):
                ans = s[i:j+1]
    return ans


print(longestPalindrome_s1("fsafsdabbad"))

########################################################################

def expandAroundCenter(s,left,right):
    while left >= 0 and right < len(s) and s[left] == s[right]:
        left -= 1
        right += 1
    return left + 1, right - 1

def longestPalindrome_s2(s):
    start, end = 0, 0
    for i in range(len(s)):
        left1, right1 = expandAroundCenter(s, i, i)
        left2, right2 = expandAroundCenter(s, i, i+1)
        if right1 - left1 > end - start:
            start, end = left1, right1
        if right2 - left2 > end - start:
            start, end = left2, right2
    return s[start: end+1]

print(longestPalindrome_s2("fsafsdabbad"))

作者:LeetCode-Solution

链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

函数调用在哪里?

今天看到一段函数,如下:

std::string comm_done_filename(const std::string & base_done_filename, const int & job_id) {
    return base_done_filename + ".done." + IntToString(job_id);
}

std::string comm_avg_model_name(const std::string & base_model_filename, const int &count) {
    return base_model_filename + ".avg." + IntToString(count);
}

隐约觉得可以用宏来解决, 因为函数名中的“done” 和“avg”,与返回值中包含的是相同的,而且两个函数形式完全一样,没有必要写成两个函数。

代码如下:

#define BT(name,flags,base,count) \
    std::string comm_##flags##_##name (const std::string & base,const int & count) { \
    return base + "."+ #flags + "." + IntToString(count); \
}

BT(model_name,avg,kaka,akak)
BT(filename,done,kaka,akak)

注意:函数名中的flags前后要有两个##, 而name只有前面有##,因为name之后没有东西了。

声名中kaka, akak是没有什么意义的,起个形参的作用。

调用的时候,当然可以直接用函数名:comm_done_filename,comm_avg_model_name。

另外,也可以使用函数指针数组或者用map。

首先,定义一个函数指针:

typedef std::string (*TransFun)(const std::string &, const int &);

函数指针数组如下:

 TransFun fun[] = {comm_done_filename,comm_avg_model_name};

map如下:

 map<string,TransFun> FunStrMap;

调用的时候当然是看不到函数名的:

 string aa = FunStrMap["done"]("cheng",3);
 string AA = fun[0]("cheng",3);

不多说,整个代码如下:

#include <iostream>
#include <string>
#include <sstream>
#include <map>

using namespace std;

std::string IntToString(const int &i) {
    std::stringstream ss;
    ss << i;
    return ss.str();
}
#define BT(name,flags,base,count) \
    std::string comm_##flags##_##name (const std::string & base,const int & count) { \
    return base + "."+ #flags + "." + IntToString(count); \
}

BT(model_name,avg,kaka,akak)
BT(filename,done,kaka,akak)

/*
std::string comm_done_filename(const std::string & base_done_filename, const int & job_id) {
    return base_done_filename + ".done." + IntToString(job_id);
}

std::string comm_avg_model_name(const std::string & base_model_filename, const int &count) {
    return base_model_filename + ".avg." + IntToString(count);
}
*/
typedef std::string (*TransFun)(const std::string &, const int &);
int main(){

    TransFun fun[] = {comm_done_filename,comm_avg_model_name};
    map<string,TransFun> FunStrMap;
    FunStrMap["done"] = fun[0];
    //FunStrMap["avg"] = fun[1];
    FunStrMap.insert(pair<string,TransFun>("avg",fun[1]));//不同添加元素的方法
    string aa = FunStrMap["done"]("cheng",3);
    string AA = fun[0]("cheng",3);
    string bb = FunStrMap["avg"]("yu",8);
    string BB = fun[1]("yu",8);
    cout<<aa<<"\n"<<bb<<endl;
    cout<<AA<<"\n"<<BB<<endl;
    return 0;

}

输出结果:

cheng.done.3
yu.avg.8
cheng.done.3
yu.avg.8

总结一下知识点:

宏定义,函数指针,函数指针数组、map

CTC的理解

CTC技术近些年搭着End2End的顺风火了起来,查了一下原文,竟然是出在2006 Pittsburgh “International Conference on Machine Learning”的会议文章。Paper很容易找,搜索一下,能找到很多可用链接。

还是郑重地给出这篇文章的名字:

Connectionist Temporal Classification: Labelling Unsegmented
Sequence Data with Recurrent Neural Networks

要想理解这篇文章,要解决几个问题:

1. 如何实现了end2end?

2. 损失函数有什么特别?

3. 前后向算法在这个算法中的作用是什么?

其实理解这篇文章之后,这三个问题的本质是一样。

第一个问题:CTC如何实现了end2end?

在ASR领域,像DNN,LSTM这样的深度模型,输出的CD-phone的state。要做后面的文字的输出,需要加入字典(Lex)、语言模型(LM),与声学模型(AM)构成解码图后才可以。CTC越过字典与语言模型,直接输出phone或者字母(英文)、字(中文),虽然在性能上达不到其他的非e2e的模型的识别率,但这肯定是未来的大趋势,毕竟技术的发展应该是越来越简单,越来越直接。而且在实现的工程上,CTC在做识别时,也会结合LM,效果会更好。

与传统的声学模型训练相比,采用CTC作为损失函数的声学模型训练,是一种完全端到端的声学模型训练,不需要预先对数据做对齐,只需要一个输入序列和一个输出序列即可以训练。这样就不需要对数据对齐和一一标注,并且CTC直接输出序列预测的概率,不需要外部的后处理。

对于一个200帧的音频数据,假设真实的输出是7。经过神经网络后(可以dnn,rnn,lstm等),出来的序列还是200。比如说“chengyu”,将chengyu扩展到200的长度,路径有许多种。扩展的原则是:将一个字符扩展成连续多个,所有字符的长度总和为200,所有以下这几种都是合理的:

ccccc…hhhh…eeeeeee…ngyu

chengyyyyyyyyyy….uuuuuuuu

这时有一个问题,那就是像hello中有两个l,怎样处理?方法是在两个l之间插入一个无意义的\epsilon,例如hh…e…lllll\epsilonlll…o..。

RNN模型本质是对 P(z|x) 建模,其中s表示输入序列,o表示输出序列,z表示最终的label,o和l存在多对一的映射关系,即:P(z│x)=P(o|x),其中o是所有映射到z的输出序列。因此,只需要穷举出所有的o,累加一起即可得到P(z│x),从而使得RNN模型对最终的label进行建模。

第二个问题:CTC的损失函数有什么特别?

不同于用于分类的cross entropy准则和回归的MSE准则,CTC的损失函数巧妙在设计为-logP(z│x)。使P(z│x)的最大,等价于损失最小,通过求log的负值,转化成一个求最小值问题。

3. 前后算法在这个算法中的作用是什么?

要想计算出每一条可能路径的得分再将所有这些得分加起来,用蛮力方式显然是不可能,巧妙地运用语音识别中的前后向算法,即baum-welch算法可以大大减少计算量,用动态规划的算法思想有效在提高了效率。

C++之static_cast

cplusplus.com中的解释:

“static_cast can perform conversions between pointers to related classes, not only upcasts (from pointer-to-derived to pointer-to-base), but also downcasts (from pointer-to-base to pointer-to-derived). No checks are performed during runtime to guarantee that the object being converted is in fact a full object of the destination type. Therefore, it is up to the programmer to ensure that the conversion is safe. On the other side, it does not incur the overhead of the type-safety checks of dynamic_cast.

1
2
3
4
classBase {};classDerived:publicBase {};
Base * a =newBase;
Derived * b =static_cast<Derived*>(a);

This would be valid code, although b would point to an incomplete object of the class and could lead to runtime errors if dereferenced.
Therefore, static_cast is able to perform with pointers to classes not only the conversions allowed implicitly, but also their opposite conversions.
static_cast is also able to perform all conversions allowed implicitly (not only those with pointers to classes), and is also able to perform the opposite of these. It can:

  • Convert from void* to any pointer type. In this case, it guarantees that if the void* value was obtained by converting from that same pointer type, the resulting pointer value is the same.
  • Convert integers, floating-point values and enum types to enum types.

Additionally, static_cast can also perform the following:

  • Explicitly call a single-argument constructor or a conversion operator.
  • Convert to rvalue references.
  • Convert enum class values into integers or floating-point values.
  • Convert any type to void, evaluating and discarding the value.

从上面的内容来看,static_cast的功能还是很多的。记录重要的几点:

1. 相当于C语言的强制类型转换。如把int转成size_t:static_cast<size_t> i_value

2. 做基类的下行转换时,不安全,建议用dynamic_cast.

3. 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。

4. 把空指针转换成目标类型的空指针。

5. 把任何类型的表达式转换成void类型。

6. static_cast不能转换掉expression的const、volatile、或者__unaligned属性

C++之reinterpret_cast

在cplusplus.com中的解释如下:

reinterpret_cast converts any pointer type to any other pointer type, even of unrelated classes. The operation result is a simple binary copy of the value from one pointer to the other. All pointer conversions are allowed: neither the content pointed nor the pointer type itself is checked.

It can also cast pointers to or from integer types. The format in which this integer value represents a pointer is platform-specific. The only guarantee is that a pointer cast to an integer type large enough to fully contain it (such as intptr_t), is guaranteed to be able to be cast back to a valid pointer.

The conversions that can be performed by reinterpret_cast but not by static_cast are low-level operations based on reinterpreting the binary representations of the types, which on most cases results in code which is system-specific, and thus non-portable.

大意是说主要用于指针类型的转换,以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。比特位不变。安全性由程序员自己保证

判断大小端及字节交换

之前写过一篇“如何判断机器的CPU是大端还是小端?http://www.orangespeech.com/?p=89”的blog, 现在更新一下开源代码中的简洁实现:

int IsLittleEndian() {
int check = 1;
  return (*reinterpret_cast<char*>(&check) != 0);
}

C的实现如下:

 #define BIG_ENDIAN      0
 #define LITTLE_ENDIAN   1

  int little_endian(void)
  {
      short int w = 0x0001;
      char *byte = (char *) &w;
      return(byte[0] ? LITTLE_ENDIAN : BIG_ENDIAN);
  }

顺带给出字节交换的代码,同样厉害!

#define SWAP8(a) { \
  int t = ((char*)&a)[0]; ((char*)&a)[0]=((char*)&a)[7]; ((char*)&a)[7]=t;\
      t = ((char*)&a)[1]; ((char*)&a)[1]=((char*)&a)[6]; ((char*)&a)[6]=t;\
      t = ((char*)&a)[2]; ((char*)&a)[2]=((char*)&a)[5]; ((char*)&a)[5]=t;\
      t = ((char*)&a)[3]; ((char*)&a)[3]=((char*)&a)[4]; ((char*)&a)[4]=t;}
#define SWAP4(a) { \
  int t = ((char*)&a)[0]; ((char*)&a)[0]=((char*)&a)[3]; ((char*)&a)[3]=t;\
      t = ((char*)&a)[1]; ((char*)&a)[1]=((char*)&a)[2]; ((char*)&a)[2]=t;}
#define SWAP2(a) { \
  int t = ((char*)&a)[0]; ((char*)&a)[0]=((char*)&a)[1]; ((char*)&a)[1]=t;}

纯虚类与dynamic_cast

今天看到一段代码,用dynamic_cast将每个vector的成员cast到基类。但是,如果基类是一个纯虚类,似乎没有这个必要。以下是我的实验:

#include <iostream>
#include <vector>

using namespace std;

class Father{
        public:
                virtual void get()=0;
};
class Son: public Father{
        public:
                void get(){ cout<<"Son"<<endl;}
};

class Daughter: public Father{
        public:
                void get() {cout<<"Doughter"<<endl;}
};

int main(){

        vector<Father *> a;

        Son s1;
        a.push_back(&s1);
        Daughter d1;
        a.push_back(&d1);

        for(size_t i=0;i<a.size();i++){
            //Father *ts = dynamic_cast<Father *>(a[i]);
            //ts->get();
            a[i]->get();//可以获得与上两行一样的结果
        }
        return 0;
}

最近读到《资治通鉴》上一段话,觉得说得太好了,摘抄下来,供今后慢品。

 “夫信者,人君之大宝也。国保于民,民保于信;非信无以使民,非民无以保国。是故古之王者不欺四海,霸者不欺四邻,善为国者不欺其民,善为家者不欺其亲。不善者反之,欺其邻国,欺其百姓,甚者欺其兄弟,欺其父子。上不信下,下不信上,上下离心,以至于败。所利不能药其所伤,所获不能补其所亡,岂不哀哉!”