131. 分割回文串

131. 分割回文串
答案整理 加深记忆 便于复习!
这道题没怎么懂。。。。后面还要反复回看。

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。
示例 1:

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

示例 2:

输入:s = “a”
输出:[[“a”]]

提示:

1 <= s.length <= 16
s 仅由小写英文字母组成

DFS介绍

本题用到了DFS,这里对其做一些介绍(引用自知乎):

首先DFS叫做深度优先搜索,既然是搜索,必然会有个起点,也会有个终点。既然是深度优先,和BFS相比,DFS就是先一次性搜到底,再退一步,再走另一条路,再一次搜到底。想象一下你在迷宫,一个粗暴的方法就是把每条路径是试一遍。那么DFS过程中,你要退一步,就必然需要保存你走过每个点的所有信息,而且是又先后顺序的,符合后进先出的规则,那么就需要用一个栈,而递归过程中函数调用会自动产生栈帧,当你的栈的深度越来越大的时候,栈也越来越大,如果递归没有终止条件,就会爆栈了。而在退一步的过程中,你需要从当前状态回到之前的状态,那么这步操作就是回溯,回溯是递归的时候一定会产生的很自然的操作,只不过大部分情况下不需要回溯。如果你知道图,两个节点之间一条有向边连接,表示从这个点可以到那个点,那么你在DFS的过程中会产生一个图。动态规划是设置边界,然后从起点开始,从起点根据转移方程把能到达的状态全都算一遍,最终再去获得那个目标节点的状态。通常一个状态可能由多个状态到达,所以会有状态叠加。一般使用for循环写方便简洁。而如果使用记忆化搜索,在DFS的过程中记录状态,找到目标后,看看如果想知道目标状态,这个目标状态依赖什么状态,就是谁能到达它那里,然后再去算它依赖的状态,然后再去看依赖的状态,不断递归,最终回到起点,答案也就出来了。两者的基础都是整个状态图,可以说记忆化搜索和动态规划是一个东西,而DFS只是一种搜索方式。而DFS同样可以不用递归,自己模拟栈实现。

总结:递归是DFS的一种实现方式,DFS是动态规划的一种实现方式。回溯法是DFS过程中可以进行的可选操作。

方法一:回溯 + 动态规划预处理

思路与算法

由于需要求出字符串 s 的所有分割方案,因此我们考虑使用搜索 + 回溯的方法枚举所有可能的分割方法并进行判断。

假设我们当前搜索到字符串的第 i 个字符,且 s[0…i−1] 位置的所有字符已经被分割成若干个回文串,并且分割结果被放入了答案数组 ans 中,那么我们就需要枚举下一个回文串的右边界 j,使得 s[i…j] 是一个回文串。

因此,我们可以从 i 开始,从小到大依次枚举 j。对于当前枚举的 j 值,我们使用双指针的方法判断 s[i…j] 是否为回文串:如果 s[i…j] 是回文串,那么就将其加入答案数组 ans 中,并以 j+1 作为新的 i 进行下一层搜索,并在未来的回溯时将 s[i…j] 从 ans 中移除。

如果我们已经搜索完了字符串的最后一个字符,那么就找到了一种满足要求的分割方法。

细节

当我们在判断 s[i…j] 是否为回文串时,常规的方法是使用双指针分别指向 i 和 j,每次判断两个指针指向的字符是否相同,直到两个指针相遇。然而这种方法会产生重复计算,例如下面这个例子:

当 s=aaba 时,对于前 2 个字符 aa,我们有 2 种分割方法 [aa] 和 [a,a],当我们每一次搜索到字符串的第 i=2 个字符 b 时,都需要对于每个 s[i…j] 使用双指针判断其是否为回文串,这就产生了重复计算。

动态规划:求取状态转移方程很关键
在这里插入图片描述
C++:

//方法一:回溯 + 动态规划预处理
class Solution
{
private:
	vector<vector<int>> f;
	vector<vector<string>> ret;
	vector<string> ans;
	int n;

public:
	void dfs(const string& s, int i)
	{
		if (i == n)
		{
			ret.push_back(ans);
			return;
		}
		for (int j = i; j < n; ++j)
		{
			if (f[i][j])
			{
				ans.push_back(s.substr(i, j - i + 1));//截取字符串s区间[i,j-i+1)的值
				dfs(s, j + 1);
				ans.pop_back();//弹出尾部元素,vector长度减一
			}
		}
	}
	vector<vector<string>> partition(string s)
	{
		n = s.size();
		//匿名数组可以直接当作参数传递,可以不使用临时变量存储
		//vector<int>(n, true) 匿名vector数组,长度为n,初始化为1
		f.assign(n, vector<int>(n, true));
		for (int i = n - 1; i >= 0; --i)
		{
			for (int j = i + 1; j < n; ++j)
			{
				f[i][j] = (s[i] == s[j]) && f[i + 1][j - 1];
			}
		}
		dfs(s, 0);
		return ret;
	}
};

复杂度分析

时间复杂度:O(n⋅2n),其中 n 是字符串 s 的长度。在最坏情况下,s 包含 n 个完全相同的字符,因此它的任意一种划分方法都满足要求。而长度为 n 的字符串的划分方案数为 2n−1=O(2n),每一种划分方法需要 O(n) 的时间求出对应的划分结果并放入答案,因此总时间复杂度为 O(n⋅2n)。尽管动态规划预处理需要 O(n2) 的时间,但在渐进意义下小于 O(n⋅2n),因此可以忽略。

空间复杂度:O(n2),这里不计算返回答案占用的空间,leetcode中计算空间时,不用计算返回答案所占用的空间。数组 f 需要使用的空间为 O(n2),而在回溯的过程中,我们需要使用 O(n) 的栈空间以及 O(n) 的用来存储当前字符串分割方法的空间。由于 O(n) 在渐进意义下小于 O(n2),因此空间复杂度为 O(n2)。

方法二:回溯 + 记忆化搜索

思路与算法

方法一中的动态规划预处理计算出了任意的 s[i…j] 是否为回文串,我们也可以将这一步改为记忆化搜索。

C++:

class Solution
{
private:
	vector<vector<int>> f;
	vector<vector<string>> ret;
	vector<string> ans;
	int n;
public:
	void dfs(const string& s, int i)
	{
		if (i == n)
		{
			ret.push_back(ans);
			return;
		}
		for (int j = i; j < n; ++j)
		{
			if (isPalindrome(s, i, j) == 1)//Palindrome 回文
			{
				ans.push_back(s.substr(i, j - i + 1));
				dfs(s, j + 1);
				ans.pop_back();
			}
		}
	}
	 记忆化搜索中,f[i][j] = 0 表示未搜索,1 表示是回文串,-1 表示不是回文串
	int isPalindrome(const string& s, int i, int j)
	{
		if (f[i][j])
		{
			return f[i][j];
		}
		if (i >= j)
		{
			return f[i][j] = 1;
		}
		return f[i][j] = (s[i] == s[j] ? isPalindrome(s, i + 1, j - 1) : -1);
	}
	vector<vector<string>>partition(string s)//partition 分割
	{
		n = s.size();
		// assign()函数原型
		//(1)void assign(const_iterator first,const_iterator last);第一个相当于个拷贝函数,把将区间[first,last)的值赋值给调用者;
		//(2)void assign(size_type n,const T& x = T());第二个把n个x赋值给调用者
		f.assign(n, vector<int>(n));

		dfs(s, 0);
		return ret;
	}
};

复杂度分析

时间复杂度:O(n⋅2n),其中 n 是字符串 s 的长度,与方法一相同。

空间复杂度:O(n2),与方法一相同。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙叙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值