KMP|DFS回溯剪枝
#1、NC149kmp
初步思路:
两层for循环,一个T的字符开始与
S的字符比较,挨个比较,遇到不同就continue当前T的字符,重复步骤=》效率太低,超时
eg:
T=ABSABABABD
S=ABABD
S!=A时,退回到A与S比较,发现不同;
继续比较A与A,B与B…A与D,不相等了了。
此时,D前面有两个AB,所以指向S的字符索引只往前移动两个字符,退回到AB|(这里)ABD,即第一个AB公共前缀后面的A这里。
继续A与A比较…
KMP的核心思想是:不必退回到开始的地方!比较过的地方就不用重复比较了!
用一个数组去记录应该退回到哪里合适
next[j - 1] 表示在 S[0…j-1] 这个子字符串中,最长的相同前缀和后缀的长度。如果 S[j] 和当前要比较的字符不匹配,这意味着 S[j] 不能作为前缀的一部分,因此需要找到一个更短的前缀,使得 S[0…next[j-1]] 和 S[i…] 能够匹配。
while 循环用于在 next 数组构建过程中,当当前字符 S.charAt(i) 与 S.charAt(j) 不匹配时,回溯找到 j 的下一个有效值。这个值是 S[0…j-1] 的最长相同前缀和后缀的长度。
为什么 while 在 if 前面:
当 S.charAt(j) != S.charAt(i) 时,说明当前 j 所对应的前缀无法与 i 位置的字符匹配。此时,需要减小 j 的值,直到找到一个匹配的前缀或者 j 减到 0。
while 循环确保即使在多次不匹配的情况下,j 也能正确地回溯到一个有效的值。如果使用 if 语句,那么在 j > 0 且 S.charAt(j) != S.charAt(i) 时,j 只会减少 1,这可能不会找到正确的前缀匹配。
if 条件的作用:当 S.charAt(j) == S.charAt(i) 时,说明当前字符匹配,j 可以安全地向前移动一位,因为 S[0…j] 和 S[i…](从 i 开始的剩余字符串)有一个共同的字符可以匹配。
next[i] = j; 的意义:
无论 S.charAt(j) 和 S.charAt(i) 是否匹配,next[i] 都被设置为 j 的当前值。
如果字符匹配,j 已经增加了 1,next[i] 反映了这个增加;
如果字符不匹配,j 通过 while 循环回溯到了正确的值,next[i] 反映了这个回溯。
public int[] getNext(String S) {
int l = S.length();
// 获得next数组
int[] next = new int[l];
next[0] = 0; //没有公共前缀
int j = 0;
// AABAS
// 01
for (int i = 1; i < l; i++) {
while (j > 0 && S.charAt(j) != S.charAt(i)) {
j = next[j - 1]; //会退一步判断
// :next[j - 1] 表示在 S[0...j-1] 这个子字符串中,最长的相同前缀和后缀的长度。如果 S[j] 和当前要比较的字符不匹配,这意味着 S[j] 不能作为前缀的一部分,因此需要找到一个更短的前缀,使得 S[0...next[j-1]] 和 S[i...] 能够匹配。
}
if (S.charAt(j) == S.charAt(i)) {
j++;
}
next[i] = j; //验证到了这一步了。
}
return next;
}
通过相同的思路使用next[]数组,
先while退回到合适的索引位置(针对S而言)
然后是if判断,S的索引++;
如果S的索引走到了S的末尾,那么说明已在T中找到等于S 的字串。
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算模板串S在文本串T中出现了多少次
* @param S string字符串 模板串
* @param T string字符串 文本串
* @return int整型
*/
public int kmp (String S, String T) {
// write code here
int sl=S.length();
int tl=T.length();
int count=0;
int[] next=getNext(S);
int j=0;
for(int i=0;i<tl;i++)
{
while(j>0&&S.charAt(j)!=T.charAt(i))
{
j=next[j-1];
}
if(S.charAt(j)==T.charAt(i))
{
j++;
}
if(j==sl)
{
count++;
j=next[j-1];//会退回去继续判断
}
}
return count;
}
public int[] getNext(String S) {
int l = S.length();
// 获得next数组
int[] next = new int[l];
next[0] = 0; //没有公共前缀
int j = 0;
// AABAS
// 01
for (int i = 1; i < l; i++) {
while (j > 0 && S.charAt(j) != S.charAt(i)) {
j = next[j - 1]; //会退一步判断
// :next[j - 1] 表示在 S[0...j-1] 这个子字符串中,最长的相同前缀和后缀的长度。如果 S[j] 和当前要比较的字符不匹配,这意味着 S[j] 不能作为前缀的一部分,因此需要找到一个更短的前缀,使得 S[0...next[j-1]] 和 S[i...] 能够匹配。
}
if (S.charAt(j) == S.charAt(i)) {
j++;
}
next[i] = j; //验证到了这一步了。
}
return next;
}
}
第二次出现的j=next[j-1]; 不是在 “j 往前走的时候已经经历过了” 的位置停止,而是在每次找到匹配或确定不匹配后,为下一次可能的匹配做准备。将 j 设置为 next[j-1] 允许我们在 T 的下一个字符处继续搜索。这是因为 S[0…next[j-1]] 已经是一个匹配的前缀,我们可以从这个前缀的末尾开始,尝试与 T 中的下一个字符匹配。
避免重复匹配:如果我们将 j 重置为 0,那么我们会在 T 中重复计算已经匹配的 S 的部分。使用 next[j-1] 确保我们不会重复计算,并且可以继续从 T 的下一个字符开始搜索。
优化搜索过程:通过这种方式,KMP 算法可以在找到一次匹配后,快速跳到可能的下一个匹配位置,而不必从头开始搜索,从而提高搜索效率。
#2、DFS回溯剪枝法
想到了树,不同的子节点,深度遍历下去有不同的路径结果。
分析题意:
X.X.X.X 每个X∈[0,255], 要求不同出现0M,M∈[0,9]
剩余X的个数1<=字符串的长度<=剩余X的个数3(因为X是1到3位数组成)
树有不同的子节点,子节点的子节点…
即树的不同层代表着字符串s的剩余长度,即当前走到了s的哪一位了(索引位置cur)。
public void dfs(String s,int cur)
{
if(tmp.size()==4&& cur==s.length())
{
res.add(tmp.get(0)+"."+tmp.get(1)+"."+tmp.get(2)+"."+tmp.get(3));
}
//jianzhi
if(s.length()-cur>3*(4-tmp.size()))//每段大于3
{
return;
}
if(s.length()-cur<4-tmp.size())//小于1
{
return;
}
int num=0;
//当前节点的操作,即X.X.X.X中的一个X
//X:从s的某个位置(索引cur)开始,i∈[cur,cur+2],且i<s.length()
for(int i=cur;i<cur+3&&i<s.length();i++)//一个一个数字的判断
{//0,1,2
num=num*10+(s.charAt(i)-'0');//数字
//百十个位
if(num<0||num>255)//数字越界了就不要了,
{
break;
}
//
tmp.add(s.substring(cur,i+1));//截取【0,255】之间的数字
//判断写一个位置上可能的X
dfs(s,i+1);//继续判断
tmp.remove(tmp.size()-1);//为啥?
// 回溯允许算法“回到”上一个状态,重新考虑不同的数字组合。
if(num==0)
{
break;//只能0.不能02|09这些
}
}
}
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param s string字符串
* @return string字符串ArrayList
*/
ArrayList<String> res=new ArrayList<>();
ArrayList<String> tmp=new ArrayList<>();
public ArrayList<String> restoreIpAddresses (String s) {
// write code here
// 限制每一个[1,3]位置以及如果是3,那么数字小于255,否则,挪给别的,
// >1则不能以0开头
// 根据位数判断
dfs(s,0);
return res;
}
public void dfs(String s,int cur)
{
if(tmp.size()==4&& cur==s.length())
{
res.add(tmp.get(0)+"."+tmp.get(1)+"."+tmp.get(2)+"."+tmp.get(3));
}
//jianzhi
if(s.length()-cur>3*(4-tmp.size()))//每段大于3
{
return;
}
if(s.length()-cur<4-tmp.size())//小于1
{
return;
}
int num=0;
for(int i=cur;i<cur+3&&i<s.length();i++)//一个一个数字的判断
{//0,1,2
num=num*10+(s.charAt(i)-'0');//数字
if(num<0||num>255)//数字越界了就不要了,
{
break;
}
tmp.add(s.substring(cur,i+1));//截取【0,255】之间的数字
dfs(s,i+1);//继续判断
tmp.remove(tmp.size()-1);//为啥?
// 回溯允许算法“回到”上一个状态,重新考虑不同的数字组合。
if(num==0)
{
break;//只能0.不能02|09这些
}
}
}
}