未出现的子串--动态规划

http://www.rqnoj.cn/Problem_90.html


题目:未出现的子串

问题编号:90

题目描述

[说明]此题中的子数字串,数字并不一定连续出现在母数字串中.比如我们定义1 3 是串1 5 3
的一个子串,但3 5 不是1 5 3 的一个子串.
串1 5 3 的所有子串为:
1
5
3
1 5
5 3
1 3
1 5 3
共7 个.
[题目描述]有一个长度为n 的数字串,其中会出现数字1,2,3,...,q(5<=q<=9).SubRaY 遇到的问
题是,需要求出一个长度最小的串(出现的数字也是1..q),使得该串不是这个数字串的子串.为
了简化问题,你只需要输出这个串的长度即可.
例如对于数字串S=
1 3 5 2 4 1 3 5 2 2 2 2 3 4 1 5 3 2(q=5)
长度为1 和2 的数字子串全出现过,但是你找不出子串S'=4 4 4.因此答案是3
[数据范围]
对于30%的数据,1<=n<=20,q=5
对于100%的数据,1<=n<=100000,5<=q<=9

输入格式

第一行两个数,串长n 和出现的数字的个数q
接下来n 行表示该数字串每一位的数字.

输出格式

未出现的子串的最小长度

样例输入

18 5
1
3
5
2
4
1
3
5
2
2
2
2
3
4
1
5
3
2

样例输出


3


一、思路

f[a[i]] 表示第i个数的最大无缺字串长度

即第i个数后面的 1 2 3 --q都有了,那么长度该数最大无缺长度为2

第j个数后面的 min(f[1]---f[q] ) + 1就是第j个数的最大无缺字串长度


二、算法

f[a[i]] = min(f[j]) + 1 1<=j<=q

1、读入n,q

2、读入每个数

3、动态规划求解

第一重循环: 从n-1倒序

第二重循环:从1-q求最小长度

    f[a[i]] = min + 1; 

4、输出

求出f[1] - f[q]中最小长度min

那么所有-1-q的最大无缺字串长度为min

即长度为1到min的字串都有,而最小缺少的字串长度为 

min + 1


代码如下:

AC


#include <iostream.h>
#include <fstream.h>

int main()
{
	int n, min, q;
	int a[100001], f[10] = {0};
	int i, j, k;
	//ifstream inFile("e:\\test.txt");
	//读入n
	cin>>n>>q;
	//inFile>>n>>q;
	//读入每个节点数
	for (i=1; i<=n; i++)
	{
		cin>>a[i];
		//inFile>>a[i];
	}
	//动态规划求解
	for (i=n; i>=1; i--)
	{
		int min = 100000;
		for (j=1; j<=q; j++)
		{
			min = min>f[j] ? f[j] : min;
		}
		f[a[i]] = min + 1;
	}
	//输出
	min = 1000000;
	for (i=1; i<=q; i++)
	{
		min = min>f[i] ? f[i] : min;
		//cout<<f[i]<< ' ' ;
	}
	cout<<min+1;

	//inFile.close();
	return 0;
}


<think>我们正在解决“无重复字符的最长子串”问题,使用动态规划方法。问题描述:给定一个字符串,找出其中不含有重复字符的最长子串的长度。动态规划思路:定义状态:dp[i]表示以第i个字符结尾的最长无重复子串的长度。注意:这里第i个字符是指字符串中索引为i的字符(索引从0开始)。状态转移:我们需要考虑两种情况:1.当前字符s[i]不在之前的最长无重复子串中(即不在以s[i-1]结尾的最长无重复子串中),那么dp[i]=dp[i-1]+1。2.当前字符s[i]出现在之前的最长无重复子串中,那么我们需要找到该字符上一次出现的位置(在当前的以s[i-1]结尾的子串中),然后从该位置之后开始重新计算当前子串。具体地,设上一次出现的位置为j(在当前的子串中,即从i-dp[i-1]到i-1的范围内),那么当前以s[i]结尾的最长子串应该从j+1开始,直到i,长度为i-j。但是,我们如何知道上一次出现的位置?我们可以用一个哈希表(或数组)来记录每个字符最近一次出现的位置。因此,我们可以优化状态转移:用dp[i]表示以s[i]结尾的最长无重复子串的长度。同时,我们维护一个变量start,表示当前无重复子串的起始位置(初始start=0),或者我们可以不用start,而是用哈希表记录每个字符最近一次出现的位置。具体步骤:1.初始化一个哈希表map(或者数组),用于存储每个字符最近一次出现的索引。2.初始化dp[0]=1,因为只有一个字符,无重复。同时,map[s[0]]=0。3.初始化一个变量max_len记录最大长度,初始为1。4.遍历字符串,从i=1到n-1:如果字符s[i]没有出现过,或者最近一次出现的位置在当前考虑的子串的起始位置之前(即不在当前子串中),那么dp[i]=dp[i-1]+1。否则,如果s[i]最近一次出现的位置(记为j)在当前子串中(即j>=当前子串的起始位置),那么当前子串的起始位置应该更新为j+1,此时以s[i]结尾的子串长度为i-j。但是,我们如何知道当前子串的起始位置?实际上,我们可以维护一个起始位置start,它表示当前无重复子串的起始索引。那么:如果s[i]在start到i-1的范围内出现过(即上一次出现的位置j>=start),那么我们需要更新start为j+1,然后dp[i]=i-start+1。否则,dp[i]=dp[i-1]+1。然而,我们也可以不显式维护start,而是通过哈希表记录的位置和当前的位置来计算。但是,显式维护start可能更直观。另一种常见的动态规划思路(与上面提到的start方法类似):定义:start:当前无重复子串的起始位置(初始为0)max_len:最大长度(初始为0)哈希表map:存储字符最近出现的位置遍历每个字符(索引为i):如果当前字符s[i]在map中,并且其最近出现的位置大于等于start(说明在当前子串中重复),那么将start更新为map[s[i]]+1。然后,更新当前无重复子串的长度为i-start+1,并更新max_len。更新map中s[i]的位置为i。注意:即使s[i]在map中出现过,但如果其上一次出现的位置小于start,说明这个重复不在当前子串中,那么当前子串仍然可以包含s[i](即不重复),所以不需要移动start。因此,动态规划的状态转移可以简化为:start=max(start,map.get(s[i],-1)+1)#如果map中没有s[i],则取-1,那么-1+1=0,不会影响startcurrent_len=i-start+1max_len=max(max_len,current_len)map[s[i]]=i这个过程中,我们并没有使用dp数组,而是用了一个变量current_len记录当前长度,以及max_len记录最大值。实际上,这是一种空间优化的动态规划(因为每个状态只依赖于前一个状态和start,所以不需要数组)。但是,题目要求动态规划解法,我们可以用dp数组,也可以不用(空间优化)。这里我们按照引用[4]中的方法,使用dp数组,但引用[4]中使用了string的rfind,效率较低,我们可以改进为使用哈希表记录位置。引用[4]的代码思路:dp[i]表示以第i个字符结尾的最长无重复子串的长度。用一个字符串temp来保存当前无重复子串(从某个位置到i-1的子串),然后对于s[i]:在temp中查找s[i]的位置(从后往前找,即rfind),如果找到(位置为index),那么从index+1开始取子串,然后加上s[i],作为新的temp,同时dp[i]=temp的长度(即index+1到i的子串长度)。如果没有找到,则temp加上s[i],dp[i]=dp[i-1]+1。但是,在temp中查找字符的时间复杂度为O(n),所以整体时间复杂度为O(n^2)。我们可以用哈希表优化查找过程。因此,我们采用优化后的动态规划(使用哈希表记录字符最近出现的位置)来达到O(n)的时间复杂度。步骤(优化后的动态规划):设n为字符串长度,如果n==0,返回0。初始化:dp[0]=1max_len=1map={}#字符到索引的映射map[s[0]]=0start=0#当前无重复子串的起始位置然后从i=1开始遍历:如果s[i]在map中,并且map[s[i]]>=start(说明这个重复发生在当前子串中),那么更新start=map[s[i]]+1计算当前以s[i]结尾的子串长度:dp[i]=i-start+1更新max_len=max(max_len,dp[i])更新map中s[i]的位置为i注意:在更新start时,我们取max(start,map[s[i]]+1)是为了确保start不会回退(因为有可能map[s[i]]+1比当前start还小?实际上,如果s[i]不在当前子串中,那么map[s[i]]应该小于start,所以我们不需要移动start。因此,我们可以这样写:ifs[i]inmapandmap[s[i]]>=start:start=map[s[i]]+1dp[i]=i-start+1max_len=max(max_len,dp[i])map[s[i]]=i但是,这样写需要先判断,我们可以统一写成:start=max(start,map.get(s[i],-1)+1)#如果s[i]不在map中,则返回-1,那么-1+1=0,但是start至少是0,所以如果start比0大,就不会被更新为0。注意,这里如果s[i]出现过,但是在上一次出现在start之前,那么map[s[i]]+1可能小于start,所以用max保证start不会变小。因此,代码可以写为:ifi>=1:start=max(start,map.get(s[i],-1)+1)dp[i]=i-start+1max_len=max(max_len,dp[i])map[s[i]]=i但是,注意:当s[i]在map中,但位置在start之前,那么start保持不变,dp[i]=i-start+1,这是正确的,因为s[i]不在当前子串中。然而,这里有一个问题:当s[i]不在map中时,map.get(s[i],-1)返回-1,那么start=max(start,0)。如果start>=0,那么start不变。但是,如果start是负数?不会,因为start初始为0。因此,我们可以统一使用start=max(start,map.get(s[i],-1)+1)来更新。但是,我们还需要注意:在更新map[s[i]]时,即使之前出现过,我们也要更新为当前的位置,因为我们需要最近出现的位置。举例验证:s="abcabcbb"i=0:s[0]='a'->map={'a':0},dp[0]=1,start=0,max_len=1i=1:'b'->map中没有,start=max(0,-1+1)=max(0,0)=0->dp[1]=1-0+1=2->max_len=2,map={'a':0,'b':1}i=2:'c'->同理,dp[2]=3,max_len=3,map={'a':0,'b':1,'c':2}i=3:'a'->map中有,上次位置0,start=max(0,0+1)=1->dp[3]=3-1+1=3,max_len=3,然后更新map['a']=3i=4:'b'->map中有'b',上次位置1,start=max(1,1+1)=2->dp[4]=4-2+1=3,max_len=3,更新map['b']=4i=5:'c'->map中有'c',上次位置2,start=max(2,2+1)=3->dp[5]=5-3+1=3,max_len=3,更新map['c']=5i=6:'b'->map中有'b',上次位置4,start=max(3,4+1)=5->dp[6]=6-5+1=2,max_len=3,更新map['b']=6i=7:'b'->map中有'b',上次位置6,start=max(5,6+1)=7->dp[7]=7-7+1=1,max_len=3,更新map['b']=7结果正确,最大长度为3。但是,我们也可以不使用dp数组,因为每次只用到前一个位置?实际上,我们并没有用到dp[i-1],而是直接计算dp[i](通过start和i),所以可以只用一个变量current_len代替dp[i],然后更新max_len。不过题目要求动态规划,我们可以用dp数组,但空间复杂度为O(n)。如果不需要保存每个位置的状态,我们可以只用一个变量记录当前长度。不过,引用[4]中使用了dp数组,我们也按照dp数组的方式写,但实际计算中,dp[i]只依赖于start(而start依赖于之前的状态),所以使用dp数组是可行的。代码实现(使用dp数组):步骤:初始化dp=[0]*ndp[0]=1max_len=1map={s[0]:0}start=0foriinrange(1,n):#更新startifs[i]inmap:#注意:map[s[i]]是上一次出现的位置,可能小于当前的start,所以用maxstart=max(start,map[s[i]]+1)else:#不在map中,start不变(但是我们可以统一用max,所以不需要else)pass#统一写法:无论是否在map中,都可以用start=max(start,map.get(s[i],-1)+1)#所以我们用统一写法start=max(start,map.get(s[i],-1)+1)dp[i]=i-start+1#更新mapmap[s[i]]=iifdp[i]>max_len:max_len=dp[i]返回max_len但是,这里有一个问题:我们在更新map[s[i]]之前,先使用了map.get(s[i],-1),这样在map中可能已经存在s[i](但我们要用更新前的位置),所以正确。注意:在更新start时,我们使用的是更新前map中的位置(即上一次出现的位置)。我们也可以不使用dp数组,而只用一个变量current_len,因为dp[i]只用于更新max_len,且不需要保存整个数组。下面给出两种代码(使用dp数组和不使用dp数组)?由于题目要求动态规划解法,我们可以使用dp数组(尽管空间复杂度为O(n)),但也可以说明动态规划的思想。但是,引用[4]中使用了dp数组,所以我们按照dp数组的方式写。然而,上述过程中,dp[i]并不依赖于dp[i-1],而是直接由start和i计算得到,所以严格来说不是典型的动态规划(状态转移不是从dp[i-1]到dp[i]),但也是动态规划的一种(状态dp[i]表示以i结尾的子串的最长长度,且状态转移通过start来间接实现)。另一种典型的动态规划是:dp[i]表示以第i个字符结尾的最长无重复子串的长度。那么,设s[i]上一次出现的位置为j(如果没有则为-1),则:dp[i]=min(dp[i-1]+1,i-j)为什么?因为:如果当前字符s[i]与之前子串中的字符不重复,那么dp[i]=dp[i-1]+1。如果重复了,那么以i结尾的最长子串的长度就是i-j(因为重复发生在j位置,所以从j+1到i是无重复的)。但是,这里有一个条件:j必须在[i-dp[i-1],i-1]的范围内,否则这个重复不在当前子串中,那么dp[i]还是dp[i-1]+1。然而,我们无法直接知道j是否在当前的子串中(即是否在[i-dp[i-1],i-1]范围内)。所以我们可以这样写:dp[i]=min(dp[i-1]+1,i-j)当j>=0时,但这样写不一定正确。例如:s="abba",当i=3(最后一个'a')时:i=0:'a'->dp[0]=1,j=-1->dp[0]=1i=1:'b'->dp[1]=2,j=-1->dp[1]=2i=2:'b'->j=1(因为上一个'b'在位置1),那么dp[2]=min(dp[1]+1=3,2-1=1)=1->正确(因为"abb"中,以第二个'b'结尾的最长子串是"b",长度为1?不对,应该是"ab"已经包含了第一个'b',所以第二个'b'重复了,那么以第二个'b'结尾的最长子串应该是从上一个'b'后面开始,即位置2开始,长度为1,所以dp[2]=1。i=3:'a'->j=0(第一个'a'),那么dp[3]=min(dp[2]+1=2,3-0=3)->取2。但是,以第三个字符(第二个'b')结尾的最长子串是"b"(长度为1),那么以第四个字符'a'结尾的子串:因为上一个'a'在位置0,而当前子串的起始位置应该是上一个重复字符'b'的位置(位置2)和上一个'a'的位置(位置0)中较大的那个?因为当前子串必须保证无重复,所以起始位置应该是max(0+1,2+1)=3?不对,应该是从位置2开始(因为位置2是上一个重复的'b'后面,但位置0的'a'在位置2之前,且当前子串(从位置2开始)中并没有包含'a',所以可以加入。但实际上,从位置2开始的子串是"ba",包含当前字符'a',所以长度应该是2。因此,我们需要知道当前子串的起始位置,也就是上一个重复字符出现的位置(但可能有多个重复?)实际上,我们关心的是上一个与当前字符相同的字符的位置,以及上一个重复事件导致的起始位置(即dp[i-1]对应的起始位置)。所以,我们仍然需要记录起始位置。所以,更通用的方法还是用start记录当前无重复子串的起始位置。因此,我们采用第一种方法(用start和哈希表)来实现动态规划。总结动态规划思想:我们定义dp[i]为以s[i]结尾的最长无重复子串的长度。通过维护一个哈希表记录每个字符最近出现的位置,以及一个变量start表示当前无重复子串的起始位置(这个起始位置是动态更新的)。状态转移方程:start=max(start,last_occurrence.get(s[i],-1)+1)dp[i]=i-start+1然后更新last_occurrence[s[i]]=i最后,最大长度就是dp数组中的最大值。我们也可以不使用dp数组,而用一个变量current_length代替dp[i],然后实时更新最大长度。但为了符合动态规划的形式,我们可以保留dp数组。代码实现(Python):deflengthOfLongestSubstring(s:str)->int:n=len(s)ifn==0:return0#初始化dp=[0]*ndp[0]=1#哈希表记录字符最近出现的位置last_occurrence={s[0]:0}start=0max_len=1foriinrange(1,n):#计算start#如果s[i]在last_occurrence中,则取last_occurrence[s[i]],否则取-1prev_index=last_occurrence.get(s[i],-1)#更新start:如果prev_index+1大于当前start,则更新,否则不变start=max(start,prev_index+1)dp[i]=i-start+1#更新最大长度ifdp[i]>max_len:max_len=dp[i]#更新字符s[i]的位置last_occurrence[s[i]]=ireturnmax_len测试例子:s="abcabcbb"->3s="bbbbb"->1s="pwwkew"->3注意:在遍历过程中,start可能被更新(增大),而dp[i]则计算了以i结尾的子串长度。这种动态规划的时间复杂度为O(n),空间复杂度为O(n)(dp数组)和O(字符集大小)(哈希表)。我们也可以不使用dp数组,只用一个变量current_len,因为dp[i]只用于更新max_len,不需要保存所有状态:deflengthOfLongestSubstring(s:str)->int:n=len(s)ifn==0:return0last_occurrence={}start=0max_len=0foriinrange(n):#如果当前字符已经出现过,并且上一次出现的位置大于等于start,则更新startifs[i]inlast_occurrenceandlast_occurrence[s[i]]>=start:start=last_occurrence[s[i]]+1#当前子串长度current_len=i-start+1ifcurrent_len>max_len:max_len=current_lenlast_occurrence[s[i]]=ireturnmax_len或者用统一写法(避免if):deflengthOfLongestSubstring(s:str)->int:start=0max_len=0last_occurrence={}fori,cinenumerate(s):ifcinlast_occurrence:#注意,这里必须保证last_occurrence[c]>=start才需要更新start,否则start不变start=max(start,last_occurrence[c]+1)current_len=i-start+1max_len=max(max_len,current_len)last_occurrence[c]=ireturnmax_len这种写法和上面一样,且更简洁。所以,我们通常采用这种空间优化的动态规划(或者称为滑动窗口更合适?)。但是,动态规划的思想体现在我们用状态转移(通过start)来计算当前子串的长度。回到问题,用户要求动态规划解决思路,我们可以给出两种形式:使用dp数组和空间优化(不使用dp数组)的形式。但空间优化形式更常用。在回答中,我们将详细解释动态规划的思想和步骤,并给出代码。</think>### 无重复字符的最长子串动态规划解法详解 #### 动态规划思想 解决**无重复字符的最长子串**问题时,动态规划的核心思路是: **定义状态**:$dp[i]$ 表示以字符串中第 $i$ 个字符结尾的最长无重复子串的长度。 **目标**:遍历整个字符串,计算所有 $dp[i]$ 的最大值,即全局最长无重复子串的长度。 #### 关键步骤与状态转移 1. **状态初始化**: - $dp[0] = 1$(单个字符无重复) - 哈希表 `last_occurrence` 记录字符最近出现的位置(如 `{'a': 0}`) - 维护指针 `start` 表示当前无重复子串的起始位置(初始为 0) 2. **状态转移方程**($i \geq 1$): $$ \begin{cases} \text{start} = \max(\text{start}, \text{last\_occurrence}[s[i]] + 1) \\ dp[i] = i - \text{start} + 1 \end{cases} $$ - **`start` 更新逻辑**: - 若 $s[i]$ 出现过或不在当前子串中(`last_occurrence.get(s[i], -1) < start`),`start` 不变。 - 若 $s[i]$ 已在当前子串中(`last_occurrence[s[i]] \geq \text{start}`),将 `start` 移到该字符上一次出现位置的下一位。 - **$dp[i]$ 计算**:以 $s[i]$ 结尾的子串长度为 $i - \text{start} + 1$。 3. **更新哈希表**: 记录 $s[i]$ 的最新位置:`last_occurrence[s[i]] = i`。 4. **更新全局最大值**: 比较所有 $dp[i]$,取最大值 $\text{max\_len} = \max(\text{max\_len}, dp[i])$。 #### 示例分析($s = \text{"pwwkew"}$) | $i$ | $s[i]$ | `start` 更新 | $dp[i]$ | 当前子串 | `last_occurrence` | |-----|--------|--------------|---------|----------|-------------------| | 0 | `'p'` | 0 | 1 | `"p"` | `{'p':0}` | | 1 | `'w'` | 0 | 2 | `"pw"` | `{'p':0, 'w':1}` | | 2 | `'w'` | $\max(0,1+1)=2$ | 1 | `"w"` | `{'p':0, 'w':2}` | | 3 | `'k'` | 2 | 2 | `"wk"` | `{'p':0, 'w':2, 'k':3}` | | 4 | `'e'` | 2 | 3 | `"wke"` | 更新 `'e':4` | | 5 | `'w'` | $\max(2,2+1)=3$ | 3 | `"kew"` | 更新 `'w':5` | 最终 $\text{max\_len} = 3$(子串 `"wke"` 或 `"kew"`)。 #### 代码实现(Python) ```python def lengthOfLongestSubstring(s: str) -> int: n = len(s) if n == 0: return 0 dp = [0] * n dp[0] = 1 last_occurrence = {s[0]: 0} start = 0 max_len = 1 for i in range(1, n): # 更新子串起始位置 start = max(start, last_occurrence.get(s[i], -1) + 1) # 计算dp[i] dp[i] = i - start + 1 # 更新最大值和字符位置 if dp[i] > max_len: max_len = dp[i] last_occurrence[s[i]] = i return max_len ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,遍历字符串一次。 - **空间复杂度**:$O(|\Sigma|)$($\Sigma$ 为字符集大小),哈希表存储字符位置[^4]。 #### 优化:空间压缩 可省略 $dp$ 数组,仅用变量 `cur_len` 记录当前子串长度: ```python def lengthOfLongestSubstring(s: str) -> int: start = 0 max_len = 0 last_occurrence = {} for i, char in enumerate(s): if char in last_occurrence and last_occurrence[char] >= start: start = last_occurrence[char] + 1 cur_len = i - start + 1 max_len = max(max_len, cur_len) last_occurrence[char] = i return max_len ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值