动态规划 —— 回文串(数)

今天是周日,看着身边的人都去踏青了,而我泡在实验室刷了将近 10 题回文数(串)相关的题目,这脑袋,也不知道被啥踢了。。但愿苦心人,天不负吧。。。

回文串的相关题目,变化还是不少的。本博客一点点呈现。题目包括:

(1)判断回文串(数)

(2)统计回文个数(将两个字符串混合)

(3)回文数猜想

(4)回文链表(三种方法)

(5)字符串的最长回文子串

(6)回文子序列个数


1、什么是回文串(数)

回文串是正读和反读都一样的字符串。比如 level ,noon 等等。

2、判断一个字符串是回文串

判断方法就是字两端的字符是否相等。很简单,代码如下:

#include
#include

using namespace std;

int main()
{
	string str;
	int flag = 1;
	while (cin >> str)
	{
		for (int i = 0; i < str.size()/2; i++)
		{
			if (str[i] != str[str.size()-1 - i])  // 首尾相等
				flag = 0;
		}

	}

	if (flag == 0)
		cout << "不是回文" << endl;
	else
		cout << "是回文" << endl;
	return 0;
}

更方便的判断方法,利用 STL 的反转函数 reverse();

// 判断字符串是不是回文
int main()
{
	string str;
	cin >> str;
	string temp_str = str;
	reverse(temp_str.begin(),temp_str.end()); // 将拷贝后的字符串反转
	if (str == temp_str)  // 如果一个字符串和其反转后的字符串相等,说明是回文
	{
		cout << "是回文" << endl;
	}
	else
		cout << "不是回文" << endl;

	return 0;
}

【华中科技大学考研复试上机题】

给出一个长度不超过1000的字符串,判断它是不是回文(顺读,逆读均相同)的。
输入描述:
输入包括一行字符串,其长度不超过1000。
输出描述:
可能有多组测试数据,对于每组数据,如果是回文字符串则输出"Yes!”,否则输出"No!"。
示例1
输入

hellolleh
helloworld
输出
Yes!
No!

#include
#include
#include

using namespace std;

void is_huiwen(string s)
{
	int flag = 1;
	for (int i = 0; i < s.size() / 2; i++)
	{
		if (s[i] != s[s.size() - 1 - i])  // 首尾相等
			flag = 0;
	}
	if (flag == 0)
		cout << "No!" << endl;
	else
		cout << "Yes!" << endl;
}

int main()
{
	vector v_str;
	string str;
	while (getline(cin, str))
	{
		v_str.push_back(str);
	}

	for (int i = 0; i < v_str.size(); i++)
		//	cout << v_str[i] << endl;
	{
		// 判断每一行字符串是不是回文
		is_huiwen(v_str[i]);
	}
}

3、统计回文(将两个字符串混合,能组成多少回文)【牛客网 2017校招真题】

题目描述:

“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。花花非常喜欢这种拥有对称美的回文串,生日的时候她得到两个礼物分别是字符串A和字符串B。现在她非常好奇有没有办法将字符串B插入字符串A使产生的字符串是一个回文串。你接受花花的请求,帮助她寻找有多少种插入办法可以使新串是一个回文串。如果字符串B插入的位置不同就考虑为不一样的办法。
例如:
A = “aba”,B = “b”。这里有4种把B插入A的办法:
* 在A的第一个字母之前: "baba" 不是回文 
* 在第一个字母‘a’之后: "abba" 是回文 
* 在字母‘b’之后: "abba" 是回文 
* 在第二个字母'a'之后 "abab" 不是回文 
所以满足条件的答案为2
输入描述:
每组输入数据共两行。
第一行为字符串A
第二行为字符串B
字符串长度均小于100且只包含小写字母
输出描述:
输出一个数字,表示把字符串B插入字符串A之后构成一个回文串的方法数
示例1
输入
aba
b
输出
2

代码:

#include
#include

using namespace std;

// 判断字符串是否为回文
bool is_huiwen(string s)
{
	int flag = 1;
	for (int i = 0; i < s.size() / 2; i++)
	{
		if (s[i] != s[s.size() - 1 - i])  // 首尾相等
			flag = 0;
	}
	if (flag == 0)
		//cout << "不是回文" << endl;
        return false;
	else
		//cout << "是回文" << endl;
        return true;
}

int main()
{
    string A,B;
    getline(cin,A);
    getline(cin,B);
    int sum = 0;
    for(int i = 0;i <= A.size();i++)  // 插入。可以插入 A.size() + 1 个位置
    {
        string temp = A;  // 引入一个中间变量,不能改变 字符串 A 
        temp.insert(i,B);
        if(is_huiwen(temp))
        {
            sum ++;
        }
    }
    cout << sum;
    return 0;
}

4、回文数猜想【ACM 题库训练】

题目描述:

题目描述
一个正整数,如果从左向右读(称之为正序数)和从右向左读(称之为倒序数)是一样的,这样的数就叫回文数。任取一个正整数,如果不是回文数,将该数与他的倒序数相加,若其和不是回文数,则重复上述步骤,一直到获得回文数为止。例如:68变成154(68+86),再变成605(154+451),最后变成1111(605+506),而1111是回文数。于是有数学家提出一个猜想:不论开始是什么正整数,在经过有限次正序数和倒序数相加的步骤后,都会得到一个回文数。至今为止还不知道这个猜想是对还是错。现在请你编程序验证之。
输入描述:
每行一个正整数。
       
特别说明:输入的数据保证中间结果小于2^31。
输出描述:
对应每个输入,输出两行,一行是变换的次数,一行是变换的过程。
示例1
输入
27228
37649
输出
3
27228--->109500--->115401--->219912
2
37649--->132322--->355553

代码:

#include
#include
#include

using namespace std;

int reverse_num(int num)
{
	int temp = 0;
	while (num != 0)
	{
		temp = temp * 10 + num % 10;
		num /= 10;
	}
	return temp;
}


bool is_huiwen(string s)   // 判断字符串是不是回文
{
	int flag = 1; // 假设是回文
	for (int i = 0; i < s.size() / 2; i++)
	{
		if (s[i] != s[s.size() - 1 - i])  // 首尾相等
			flag = 0;
	}
	if (flag == 0)
		//cout << "不是回文" << endl;
		return false;
	else
		//cout << "是回文" << endl;
		return true;
}


int main()
{
	int n;
	//vector v;
//	while (cin >> n)
//	{
//		v.push_back();
//	}
	while (cin >> n)
	{
		string s;
		int jishu = 0;
		s = to_string(n);
		//cout << s;
		string s1 = s;

		while (!is_huiwen(s1))  // 如果不是回文
		{
			jishu++;
			int temp_s = stoi(s1);  // 先将字符串 s 转换为 整型数字
			int temp_i = reverse_num(temp_s); // 将整型数字逆序
			s1 = to_string(temp_s + temp_i);

		}
		std::cout << jishu << endl;  // 输出变换次数

		while (!is_huiwen(s))  // 如果不是回文
		{
			std::cout << s;
			std::cout << "--->";
			int temp_s = stoi(s);  // 先将字符串 s 转换为 整型数字
			int temp_i = reverse_num(temp_s); // 将整型数字逆序
			s = to_string(temp_s + temp_i);

		}
		std::cout << s << endl;
	}
	return 0;
}

分析:刚写的代码还是有点小问题的,但是牛客网上的编辑器给通过了。问题在于 while(cin >>n),如果输入一行,以空格隔开,结果没问题。但是一行输入一个数字,就不行了。而且,统计转换的次数,我又进行了一次循环,这无疑增大了复杂度。我是这样改的。

#include
#include

using namespace std;

int reverse_num(int num)
{
	int temp = 0;
	while (num != 0)
	{
		temp = temp * 10 + num % 10;
		num /= 10;
	}
	return temp;
}

int main()
{
	int n;
	while (cin >> n)
	{
		vector v;
		while (n != reverse_num(n))  // 判断是不是回文数字
		{
			v.push_back(n);
			n += reverse_num(n); // 如果不是回文数字,就将其本身和其反转后的数字相加
		}
		v.push_back(n);  // 循环结束后,n 已经是回文了
		cout << v.size() - 1 << endl; // 所以, n 之前的数字的个数就是进行转换的次数
		int i;
		for (i = 0; i";
		cout << v[i] << endl;
	}
	return 0;
}

5、回文链表

请编写一个函数,检查链表是否为回文。
给定一个链表ListNode* pHead,请返回一个bool,代表链表是否为回文。

测试样例:
{1,2,3,2,1}
返回:true
{1,2,3,2,3}

返回:false

代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/

// 反转链表
 ListNode* ReverseList(ListNode* pHead) {
        if(pHead == NULL)
            return pHead;
        ListNode* pNode = pHead;  //当前处理的结点
        ListNode* preNode = NULL;
        ListNode* reverseNode = NULL;
        while(pNode != NULL)
        {
            ListNode* nextNode= pNode->next;
            if(nextNode == NULL)
                reverseNode = pNode;
            
            pNode->next = preNode;  // 调整链表的指向
            preNode = pNode;
            pNode = nextNode;
        }
        return reverseNode;
    }

class Palindrome {
public:
    bool isPalindrome(ListNode* pHead) {
        // write code here
        
        // 可以考虑现将链表进行反转
        ListNode* qNode = ReverseList(pHead);
        bool flag = true; // 假设是回文
        while(pHead != NULL && qNode != NULL)
        {
            if(pHead->val != qNode->val)
                flag = false;
            pHead = pHead->next;
            qNode = qNode->next;
        }
        return flag;
    }
};

上面代码的思路是先反转链表,比较容易想。但是需要额外的空间。下面介绍不使用额外的空间,只对链表的后半部分进行反转。然后一个指针指向链表头,一个指针指向链表中间,比较值是否相等,不相等就返回false。代码如下:

class Palindrome {
public:
    bool isPalindrome(ListNode* pHead) {
        // write code here
        int count=0;
        ListNode* p=pHead;
        while(p!=NULL){
            p=p->next;
            count++;
        }
        count=count%2==0?count/2:(count/2+1);
        p=pHead;
        for(int i=0;inext;
        }
        ListNode* pre=NULL;
        while(p!=NULL){
            ListNode* temp=p->next;
            p->next=pre;
            pre=p;
            p=temp;
        }
        ListNode* m=pHead;
        while(pre!=NULL&&m!=NULL){
            if(pre->val!=m->val)
                return false;
            pre=pre->next;
            m=m->next;
        }
 
        return true;
    }
};

还有两种方法供参考:

(5-2)、快慢指针法(该类方法是解决单链表问题常用的方法)

/*
 * 1.利用快慢指针,可以找到链表中间位置
 * 2.在快慢指针扫描的同时,将慢指针所指向的节点从原链表摘下,头插法插入一个新链表
 * 3.最后只须继续扫描新链表和原链表,比较是否相同(稍微注意一下原链表长度为奇数或偶数的情况)
**/
bool isPalindrome(ListNode* pHead) {
    // write code here
    if (pHead==NULL)
        return true;
    ListNode* newHead = NULL;
    ListNode* pSlow = pHead;
    ListNode* pQuick = pHead;
    // 当原链表长度为奇数时,快指针 pQuick 刚好扫到尾节点停止
    // 当原链表长度为偶数时,快指针 pQuick 扫到尾节点前一节点停止
    while (pQuick->next!=NULL && pQuick->next->next!=NULL) {
        pQuick = pQuick->next->next;    // 快指针前进两步
        // 慢指针指向的节点从原链表删除,头插法插入新链表
        pHead = pSlow->next;
        pSlow->next = newHead;
        newHead = pSlow;
        pSlow = pHead;
    }
    pHead = pSlow->next;
    if (pQuick->next!=NULL) {
        pSlow->next = newHead;
        newHead = pSlow;
    }
    ListNode* p = pHead;
    ListNode* q = newHead;
    while (p!=NULL && q!=NULL) {
        if (p->val != q->val)
            return false;
        p = p->next;
        q = q->next;
    }
    return true;
}

或者利用栈

public class Palindrome {
    public boolean isPalindrome(ListNode pHead){
        ListNode fast = pHead;
        ListNode slow = pHead;
        Stack stack = new Stack();
        /**
         * 将链表的前半部分元素装入栈中,当快速runner
                 *(移动的速度是慢速runner的两倍)
         * 到底链表尾部时,则慢速runner已经处于链表中间位置
         */
        while(fast != null && fast.next != null){
            stack.push(slow.val);
            slow = slow.next;
            fast = fast.next.next;
        }
        //当链表为奇数个时,跳过中间元素
        if (fast != null) {
            slow = slow.next;
        }
        while(slow != null){
            //如果两者不相同,则该链表不是回文串
            if (stack.pop() != slow.val) {
                return false;
            }
            slow = slow.next;
        }
        return true;
    }
}

(5-3)、递归

采用递归的方式,将p定义成static,每次传入一个新的链表,让p指向链表首节点。通过递归,pHead从后往前,p从前往后,同时比较。

class Palindrome {
public:
    bool isPalindrome(ListNode* pHead) {
        // write code here
        if(pHead==NULL)
            return true;
        static ListNode* p=NULL;
        if(p==NULL) p=pHead;
        if(isPalindrome(pHead->next)&&(p->val==pHead->val))
        {
            p=p->next;
            return true;
        }
        p=NULL;
        return false;
    }
};

6、字符串中是否存在回文子串

7、求字符串的最长回文子串 (O(n) 时间复杂度)

这一小部分参考了博客:

https://www.felix021.com/blog/read.php?2040

https://www.cnblogs.com/AndyJee/p/4465696.html

分析:思路:
动态规划思想:

对于任意字符串,如果头尾字符相同,那么字符串的最长子序列等于去掉首尾的字符串的最长子序列加上首尾;如果首尾字符不同,则最长子序列等于去掉头的字符串的最长子序列和去掉尾的字符串的最长子序列的较大者。


因此动态规划的状态转移方程为:

设字符串为str,长度为n,p[i][j]表示第i到第j个字符间的子序列的个数(i<=j),则:
状态初始条件:dp[i][i]=1 (i=0:n-1)
状态转移方程:dp[i][j]=dp[i+1][j-1] + 2  if(str[i]==str[j])
                   dp[i][j]=max(dp[i+1][j],dp[i][j-1])  if (str[i]!=str[j])

代码:

#include 
#include 
#include
#include

using namespace std;

int longestPalindromeSubSequence1(string str){
    int n=str.length();
    vector > dp(n,vector(n));

    for(int j=0;j=0;i--){
            if(str[i]==str[j])
                dp[i][j]=dp[i+1][j-1]+2;
            else
                dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
        }
    }
    return dp[0][n-1];
}

int longestPalindromeSubSequence2(string str){
    int n=str.length();
    vector > dp(n,vector(n));

    for(int i=n-1;i>=0;i--){
        dp[i][i]=1;
        for(int j=i+1;j>s){
        length=longestPalindromeSubSequence2(s);
        cout<

8、求回文子串的个数

要求:

给定字符串,求它的回文子序列个数。回文子序列反转字符顺序后仍然与原序列相同。例如字符串aba中,回文子序列为"a", "a", "aa", "b", "aba",共5个。内容相同位置不同的子序列算不同的子序列。

动态规划思想:
对于任意字符串,如果头尾字符不相等,则字符串的回文子序列个数就等于去掉头的字符串的回文子序列个数+去掉尾的字符串的回文子序列个数-去掉头尾的字符串的回文子序列个数;如果头尾字符相等,那么除了上述的子序列个数之外,还要加上首尾相等时新增的子序列个数,1+去掉头尾的字符串的回文子序列个数,1指的是加上头尾组成的回文子序列,如aa,bb等。
因此动态规划的状态转移方程为:

设字符串为str,长度为n,p[i][j]表示第i到第j个字符间的最长子序列的长度(i<=j),则:
状态初始条件:dp[i][i]=1 (i=0:n-1)
状态转移方程:dp[i][j]=dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]  if(str[i]!=str[j])

                   dp[i][j]=dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]+dp[i+1][j-1]+1=dp[i+1][j] + dp[i][j-1]+1  if (str[i]==str[j])

代码:

#include 
#include 
using namespace std;

int NumOfPalindromeSubSequence(string str){
    int len=str.length();
    vector > dp(len,vector(len));

    for(int j=0;j=0;i--){
            dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1];
            if(str[i]==str[j])
                dp[i][j]+=1+dp[i+1][j-1];
        }
    }
    return dp[0][len-1];
}

int main()
{
    string str;
    int num;
    while(cin>>str){
        num=NumOfPalindromeSubSequence(str);
        cout<




你可能感兴趣的:(IT校招——面试/笔试题集锦,数据结构/算法)