1. 程式人生 > >動態規劃 —— 迴文串(數)

動態規劃 —— 迴文串(數)

今天是週日,看著身邊的人都去踏青了,而我泡在實驗室刷了將近 10 題迴文數(串)相關的題目,這腦袋,也不知道被啥踢了。。但願苦心人,天不負吧。。。

迴文串的相關題目,變化還是不少的。本部落格一點點呈現。題目包括:

(1)判斷迴文串(數)

(2)統計迴文個數(將兩個字串混合)

(3)迴文數猜想

(4)迴文連結串列(三種方法)

(5)字串的最長迴文子串

(6)迴文子序列個數

1、什麼是迴文串(數)

迴文串是正讀和反讀都一樣的字串。比如 level ,noon 等等。

2、判斷一個字串是迴文串

判斷方法就是字兩端的字元是否相等。很簡單,程式碼如下:

#include<iostream>
#include<string>

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<iostream>
#include<string>
#include<vector>

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<string> 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<iostream>
#include<string>

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<iostream>
#include<string>
#include<vector>

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<int> 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<iostream>
#include<vector>

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<int> 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<v.size() - 1; ++i)
			cout << v[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;i<count;++i){
            p=p->next;
        }
        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<Integer> stack = new Stack<Integer>();
        /**
         * 將連結串列的前半部分元素裝入棧中,當快速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 <iostream>
#include <vector>
#include<string>
#include<algorithm>

using namespace std;

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

    for(int j=0;j<n;j++){
        dp[j][j]=1;
        for(int i=j-1;i>=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<vector<int> > dp(n,vector<int>(n));

    for(int i=n-1;i>=0;i--){
        dp[i][i]=1;
        for(int j=i+1;j<n;j++){
            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 main()
{
    string s;
    int length;
    while(cin>>s){
        length=longestPalindromeSubSequence2(s);
        cout<<length<<endl;
    }
    return 0;
}

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 <iostream>
#include <vector>
using namespace std;

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

    for(int j=0;j<len;j++){
        dp[j][j]=1;
        for(int i=j-1;i>=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<<num<<endl;
    }
    return 0;
}