【动态规划】【滑动窗口】C++算法:100154 执行操作后的最大分割数量

作者推荐

【动态规划】【字符串】扰乱字符串

本文涉及的基础知识点

C++算法:滑动窗口总结
动态规划

LeetCode100154 执行操作后的最大分割数量

给你一个下标从 0 开始的字符串 s 和一个整数 k。
你需要执行以下分割操作,直到字符串 s 变为 空:
选择 s 的最长前缀,该前缀最多包含 k 个 不同 字符。
删除 这个前缀,并将分割数量加一。如果有剩余字符,它们在 s 中保持原来的顺序。
执行操作之 前 ,你可以将 s 中 至多一处 下标的对应字符更改为另一个小写英文字母。
在最优选择情形下改变至多一处下标对应字符后,用整数表示并返回操作结束时得到的最大分割数量。
示例 1:
输入:s = “accca”, k = 2
输出:3
解释:在此示例中,为了最大化得到的分割数量,可以将 s[2] 改为 ‘b’。
s 变为 “acbca”。
按照以下方式执行操作,直到 s 变为空:

  • 选择最长且至多包含 2 个不同字符的前缀,“acbca”。
  • 删除该前缀,s 变为 “bca”。现在分割数量为 1。
  • 选择最长且至多包含 2 个不同字符的前缀,“bca”。
  • 删除该前缀,s 变为 “a”。现在分割数量为 2。
  • 选择最长且至多包含 2 个不同字符的前缀,“a”。
  • 删除该前缀,s 变为空。现在分割数量为 3。
    因此,答案是 3。
    可以证明,分割数量不可能超过 3。
    示例 2:
    输入:s = “aabaab”, k = 3
    输出:1
    解释:在此示例中,为了最大化得到的分割数量,可以保持 s 不变。
    按照以下方式执行操作,直到 s 变为空:
  • 选择最长且至多包含 3 个不同字符的前缀,“aabaab”。
  • 删除该前缀,s 变为空。现在分割数量为 1。
    因此,答案是 1。
    可以证明,分割数量不可能超过 1。
    示例 3:
    输入:s = “xxyz”, k = 1
    输出:4
    解释:在此示例中,为了最大化得到的分割数量,可以将 s[1] 改为 ‘a’。
    s 变为 “xayz”。
    按照以下方式执行操作,直到 s 变为空:
  • 选择最长且至多包含 1 个不同字符的前缀,“xayz”。
  • 删除该前缀,s 变为 “ayz”。现在分割数量为 1。
  • 选择最长且至多包含 1 个不同字符的前缀,“ayz”。
  • 删除该前缀,s 变为 “yz”,现在分割数量为 2。
  • 选择最长且至多包含 1 个不同字符的前缀,“yz”。
  • 删除该前缀,s 变为 “z”。现在分割数量为 3。
  • 选择最且至多包含 1 个不同字符的前缀,“z”。
  • 删除该前缀,s 变为空。现在分割数量为 4。
    因此,答案是 4。
    可以证明,分割数量不可能超过 4。
    参数范围
    1 <= s.length <= 104
    s 只包含小写英文字母。
    1 <= k <= 26

分析

变量解析

dpNotChangedpNotChange[i]表示子串s[i,m_c)的最大分割数
vSplitvSplit[inx][i]=j表示:将s[i]修改成’a’+inx,其它字符不变的情况下,s[i,m_c)的最长前缀是s[i,j)

不修改任何字符的情况下,s拆分成m个子串,分别为[li1,ri1),[li2,ri2)… [lim,rim)。
显然 li1等于0,rim等于m_c, ri1等于li2…
分以下情况:

  • 一,没有改变任何字符。
  • 二,改变某个字后,[lix,rix)发生改变,变成[lix,nix),只需要考虑nix < rix,原因见下文证明一

情况二:
分两层循环:

  • 第一层循环:枚举不修改字符的子串。
  • 第二层循环:枚举nix。

两层循环的时间复杂度为:O(s.length)。
枚举nix的时候,分两种情况:
一,s[nix]没有改变,s[nix,m_c)的最大分割数就是dpNotChange[rix]。前提条件是:

  • k<26 否则无论变成k+1。
  • 包括nix,目前已经有k个不同字符
  • [lix,nix]至少有k+1个字符,由于其只有k个不同字符,所以至少有两个字符相同,去掉nix,[lix,nix)至少还有一个相同字符,将其改成k个字符以外的字符。只能证明[lix,rix]有k+1个字符,不能证明是第一个rix。假定第一个rix是rix1,rix会被rix1淘汰,所以不会造成错误。原因见证明一
    二,枚举s[nix]改成’a’到’z’。通过vSplit查询第一个前缀。

通过滑动窗口计算vSplit[inx][i-1]

s[i,rightk] 共有k个字符,rightk取最小值;是s[i,rightk1]共有k+1个字符,rightk1取最小值。如果没有足够的字符取m_c。
countk 记录了这k个字符,countk1记录了k+1个字符。

countk 包括inx+‘a’countk1的数量为k+1结果为:rightk1
countk 包括inx+‘a’countk1的数量不够k+1结果为:m_c
countk 不包括inx+‘a’countk的数量为k结果为:rightk
countk 不包括inx+‘a’countk的数量不够k结果为:m_c

证明一 i1<=12则dpNotChange[i1] >= dpNotChange[i2]

特殊情况一:如果s[i1,m_c)和s[i2,m_c) 都不到k+1个字符。则两者都为1。
特殊情况二:s[i2,m_c) 不到k+1个字符,s[i1,m_c)有k+1个字符,则前者为1,后者超过1。
s[i1,m_c) 不到k+1个字符,s[i2,m_c)有k+1个字符,这种i情况不存在。
如果两者都有k+1字符
则s[i2,m_c)可以拆分s[i2,i4)和s[i4,m_c)
s[i1,m_c)可以拆分成s[i1,i3)和s[i3,m_c)
则i3<=i4,i1变i3变大;i2变i4,每次迭代i1和i2都变大,若干次后一定会变成特殊情况一或特殊情况二。
假定i3>i4
s[i2,i4]刚好k+1个字符,那么s[i1,i2)+s[i2,i4]+s(i4+i3)的字符数大于等于k+1,这三个子串加在一起就是s[i1,i3),与s[i1,i3)只有k个字符矛盾。

代码

核心代码

template<class KEY>
class CKeyCount
{
public:
	void Add(const KEY& key, int iCount)
	{
		Cnt[key] += iCount;
		if (0 == Cnt[key])
		{
			Cnt.erase(key);
		}
	}
	std::unordered_map<KEY, int> Cnt;
};

class Solution {
public:
	int maxPartitionsAfterOperations(string s, int k) {
		m_c = s.length();
		vSplit[inx][i]=j表示:将s[i]修改成'a'+inx,其它字符不变的情况下,s[i,m_c)的最长前缀是s[i,j)
		vector<vector<int>> vSplit(26,vector<int>(m_c, m_c));		
		for (int inx = 0; inx < 26; inx++)
		{			
			CKeyCount<char> countk1,countk;	
			//s[i,rightk] 共有k个字符,rightk取最小值;是s[i,rightk1]共有k+1个字符,rightk取最小值。如果没有足够的字符取m_c
			for (int i =1, rightk1 =i-1,rightk=i-1; i < m_c;i++ )
			{	//s(left,right]和inx+'a' 刚好k+1
				while ((rightk +1 < m_c)&&(countk.Cnt.size() < k ))
				{
					countk.Add(s[++rightk],1);
				}
				while ((rightk1 + 1 < m_c) && (countk1.Cnt.size() <= k))
				{
					countk1.Add(s[++rightk1], 1);
				}
				if (countk.Cnt.count('a' + inx))
				{
					vSplit[inx][i - 1] = (k + 1 == countk1.Cnt.size()) ? rightk1 : m_c;
				}
				else
				{
					vSplit[inx][i - 1] = (k  == countk.Cnt.size()) ? rightk : m_c;
				}
				countk.Add(s[i], -1);
				countk1.Add(s[i], -1);
			}
		}

		vector<int> dpNotChange(m_c + 1, 0);//dp1[i]=x表示 ,在不修改的情况下 s[i,m_c)最多可以分割x次
		for (int i = m_c - 1; i >= 0; i--)
		{
			int inx = vSplit[s[i]-'a'][i];
			dpNotChange[i] =  dpNotChange[inx] + 1;
		}
		int iRet = dpNotChange.front();
		int iPreNum = 0;
		for (int i = 0; i < m_c; i = vSplit[s[i]-'a'][i])
		{
			iPreNum++;
			set<char> setHas;
			for (int j = i; j < vSplit[s[i] - 'a'][i]; j++)
			{
				if (setHas.size()==k)
				{
					for (char ch = 'a'; ch <= 'z'; ch++)
					{//修改了j,且s[j]是新的子串的开始
						if (!setHas.count(ch))
						{
							const int inx = vSplit[ch - 'a'][j];
							iRet = max(iRet, iPreNum + 1 + dpNotChange[inx]);
						}
					}
				}				
				setHas.emplace(s[j]);
				if ((setHas.size() == k)&&(k< 26)&&(j-i+1 > k  ))
				{
					iRet = max(iRet, iPreNum + dpNotChange[j]);
				}
			}
		}
		return iRet;
	}
	int m_c;
};

测试用例

template<class T>
void Assert(const T& t1, const T& t2)
{
	assert(t1 == t2);
}

template<class T>
void Assert(const vector<T>& v1, const vector<T>& v2)
{
	if (v1.size() != v2.size())
	{
		assert(false);
		return;
	}
	for (int i = 0; i < v1.size(); i++)
	{
		Assert(v1[i], v2[i]);
	}
}


int main()
{
	string s;
	int k;

	
	{
		Solution sln;
		s = "baac", k = 3;
		auto res = sln.maxPartitionsAfterOperations(s, k);
		Assert(2, res);
	}
	{
		Solution sln;
		s = "accca", k = 2;
		auto res = sln.maxPartitionsAfterOperations(s, k);
		Assert(3, res);
	}
	{
		Solution sln;
		s = "aabaab", k = 3;
		auto res = sln.maxPartitionsAfterOperations(s, k);
		Assert(1, res);
	}
	
	
	{
		Solution sln;
		s = "xxyz", k = 1;
		auto res = sln.maxPartitionsAfterOperations(s, k);
		Assert(4, res);
	}
}

两个错误代码

都没有考虑s[nix]会修改。

class Solution {
public:
int maxPartitionsAfterOperations(string s, int k) {
m_c = s.length();
vector<vector> vSplit(2, vector(m_c, m_c));
{
CKeyCount count;
for (int i = 0, right = 0; i < m_c; i++)
{
while ((right < m_c) && (count.Cnt.size() <= k))
{
count.Add(s[right++], 1);
}
vSplit[0][i] = right;
count.Add(s[i], -1);
}
}
{
CKeyCount count;
for (int i = 0, right = 0; i < m_c; i++)
{
while ((right < m_c) && (count.Cnt.size() < k))
{
if ((count.Cnt.size() == k) && (right - i > k))
{
break;
}
count.Add(s[right++], 1);
}
vSplit[1][i] = right;
count.Add(s[i], -1);
}
}
vector<vector> dp(2, vector(m_c+1, 0));
dp[0][0] = 0;
dp[1][1] = -m_c;
for (int i = 0; i < m_c; i++)
{
//没改字符=>没改字符
dp[0][vSplit[0][i]] = max(dp[0][vSplit[0][i]], dp[0][i] + 1);
//没改字符=>改字符
dp[1][vSplit[1][i]] = max(dp[1][vSplit[1][i]], dp[0][i] + 1);
//改字符=>改字符
dp[1][vSplit[0][i]] = max(dp[1][vSplit[0][i]], dp[1][i] + 1);
}
return max(dp[0].back(), dp[1].back());
}
int m_c;
};

class Solution {
public:
int maxPartitionsAfterOperations(string s, int k) {
m_c = s.length();
vector vSplit(m_c, m_c);//vSplit[i]=j表示,在不修改的情况s[i,m_c)的最长前缀是s[i,j)
CKeyCount count;
for (int i = 0, right = 0; i < m_c; i++)
{
while ((right < m_c) && (count.Cnt.size() <= k))
{
count.Add(s[right++], 1);
}
vSplit[i] = right;
count.Add(s[i], -1);
}

	vector<int> dpNotChange(m_c+1, 0);//dp1[i]=x表示 ,在不修改的情况下 s[i,m_c)最多可以分割x次
	for (int i = m_c - 1; i >= 0; i--)
	{
		dpNotChange[i] = max(dpNotChange[i], dpNotChange[vSplit[i]] + 1);
	}
	int iRet = dpNotChange.front();
	int iPreNum = 0;
	for (int i = 0; i < m_c; i = vSplit[i])
	{
		iPreNum++;
		CKeyCount<char> cnt;
		for (int j = i; j < vSplit[i]; j++)
		{
			cnt.Add(s[j], 1);
			if ((cnt.Cnt.size() == k) && (j - i + 1 > k))
			{
				iRet = max(iRet, iPreNum + dpNotChange[j]);
			}
		}
	}
	return iRet;
}
int m_c;

};

扩展阅读

视频课程

有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771

如何你想快

速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

相关

下载

想高屋建瓴的学习算法,请下载《喜缺全书算法册》doc版
https://download.csdn.net/download/he_zhidan/88348653

我想对大家说的话
闻缺陷则喜是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 **C+

+17**
如无特殊说明,本算法用**C++**实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/300182.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flink中的状态管理

一.Flink中的状态 1.1 概述 在Flink中&#xff0c;算子任务可以分为有状态和无状态两种状态。 无状态的算子任务只需要观察每个独立事件&#xff0c;根据当前输入的数据直接转换输出结果。例如Map、Filter、FlatMap都是属于无状态算子。 而有状态的算子任务&#xff0c;就…

C#使用栈方法遍历二叉树

步骤一&#xff1a;定义一个二叉树的节点类 定义一个二叉树的节点类&#xff0c;其中包含节点的值、左子节点和右子节点。 // 二叉树节点定义public class TreeNode{public int Value { get; set; } // 节点的值public TreeNode Left { get; set; } // 左子节点public TreeN…

uniapp 微信小程序跳转至其他小程序

一、背景&#xff1a; 需要在目前的小程序中跳转到另一个小程序&#xff0c;跳转的目标小程序需要已经发布上线了 二、具体实现 使用uni.navigateToMiniProgram打开另一个小程序 官网指引&#x1f449;&#xff1a;uni.navigateToMiniProgram(OBJECT) | uni-app官网 <t…

手把手教你用jmeter做压力测试(详图)

一.前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步…

基于机器视觉的车牌检测-车牌粗略定位

基于颜色特征的定位算法 基于颜色特征的定位算法。该算法不用对整幅图像进行边缘检测&#xff0c;而是直接寻找图片中颜色、形状及纹理符合车牌特征的连通区域。通过分析车牌图像&#xff0c;发现对于具有某种目标颜色的像素&#xff0c;可以直接通过对H、S、I三分量设定一个范…

CPU平台做视频智能分析,Lnton视频分析平台不仅支持流分析,同时也支持图片分析了

LntonAIServer最新v1.0.09版本支持图片分析了&#xff0c;经过几个月的研发&#xff0c;在原有的视频流分析的基础上&#xff0c;我们终于支持大家都非常期待的图片分析功能了&#xff0c;图片分析的功能加上&#xff0c;能有利于很多场景的展开&#xff0c;比如在烟火、明厨亮…

Java学习笔记(十)——异常

一、异常的概念 二、异常体系图&#xff08;重要&#xff09; 三、常见的异常 &#xff08;一&#xff09;常见的运行时异常 1、NullPointerException空指针异常 2、ArithmeticException数学运算异常 3、ArrayIndexOutOfBoundsException数组下标越界异常 4、ClassCastEx…

阿里巴巴微服务治理框架的终极PK!

另外我的新书RocketMQ消息中间件实战派上下册&#xff0c;在京东已经上架啦&#xff0c;目前都是5折&#xff0c;非常的实惠。 https://item.jd.com/14337086.html​编辑https://item.jd.com/14337086.html “RocketMQ消息中间件实战派上下册”是我既“Spring Cloud Alibaba微…

Java调用shell脚本实现数据库备份功能

本篇文章主要介绍怎样使用Java程序&#xff0c;执行服务器上的数据库备份Shell脚本进行MySQL数据库的备份功能。 学习目标 使用Java执行Shell脚本、实现MySQL数据库的备份功能。 学习内容 编写导出MysSQL数据库的Shell脚本 以下是一个使用Bash脚本进行数据库备份的示例代码…

java基础之Java8新特性-Stream(流)

简介 流&#xff08;Stream&#xff09;是 Java 8 引入的一种处理集合数据的抽象概念&#xff0c;它提供了一种更简洁、更灵活的方式来操作和处理集合数据。流可以看作是一系列元素的管道&#xff0c;可以对这些元素进行筛选、转换、排序、归约等操作&#xff0c;实现各种数据…

【leetcode】力扣热门之合并两个有序列表【简单难度】

题目描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 用例 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 输入&#xff1a;l1 [], l2 [] 输出&#xff1a;[] 输入&#xff1a;l1 []…

FC SAN光纤交换机维护介绍

一、什么是FC SAN &#xff1f; ​存储区域网络&#xff08;Storage Area Network&#xff0c;SAN&#xff09;采用网状通道&#xff08;Fibre Channel &#xff0c;简称FC&#xff0c;区别与Fiber Channel光纤通道&#xff09;技术&#xff0c;通过FC交换机连接存储阵列和服务…

实现目标检测中的数据格式自由(labelme json、voc、coco、yolo格式的相互转换)

在进行目标检测任务中&#xff0c;存在labelme json、voc、coco、yolo等格式。labelme json是由anylabeling、labelme等软件生成的标注格式、voc是通用目标检测框&#xff08;mmdetection、paddledetection&#xff09;所支持的格式&#xff0c;coco是通用目标检测框&#xff0…

学习笔记:C++之 switch语句

Switch语句 作用&#xff1a;执行多条件分支语句 语法&#xff1a; switch&#xff08;表达式&#xff09;{ case 结果1&#xff1a;执行语句&#xff1b;break&#xff1b; case 结果2&#xff1a;执行语句&#xff1b;break&#xff1b; ... default&#xff1a;执行语句&a…

Java反射之获取构造方法,成员变量,成员方法以及反射的作用

目录 1.什么是反射2.获取Class对象的三种方式3.反射获取构造方法4.反射获取成员变量5.反射获取成员方法6.反射的作用 1.什么是反射 在Java中&#xff0c;反射是指程序在运行时动态地获取类的信息、调用方法和访问属性的能力。 通过反射&#xff0c;可以在运行时获取类的构造函数…

什么是多因素身份验证(MFA)

多重身份验证&#xff08;MFA&#xff09;是在授予用户访问特定资源的权限之前&#xff0c;使用多重身份验证来验证用户身份的过程&#xff0c;仅使用单一因素&#xff08;传统上是用户名和密码&#xff09;来保护资源&#xff0c;使它们容易受到破坏&#xff0c;添加其他身份验…

计算机原理 (2) CPU的诞生 输入 输出 PC指针

文章目录 计算机的前世今生计算机的三个根本性基础1. 计算机是执行输入、运算、输出的机器&#xff1b;2.程序是指令和数据的集合&#xff1b;3.计算机的处理方式有时与人们的思维习惯不同 二、结论三、参考资料交个朋友 计算机的前世今生 上一篇文章最终结束的时候谈到希望给…

JavaScript异常处理实战

前言 之前在对公司的前端代码脚本错误进行排查&#xff0c;试图降低 JS Error 的错误量&#xff0c;结合自己之前的经验对这方面内容进行了实践并总结&#xff0c;下面就此谈谈我对前端代码异常监控的一些见解。 本文大致围绕下面几点展开讨论&#xff1a; JS 处理异常的方式…

鸿蒙开发基础运用(ArkTS)-健康生活APP

健康生活应用&#xff0c;主要功能包括&#xff1a; 用户可以创建最多6个健康生活任务&#xff08;早起&#xff0c;喝水&#xff0c;吃苹果&#xff0c;每日微笑&#xff0c;刷牙&#xff0c;早睡&#xff09;&#xff0c;并设置任务目标、是否开启提醒、提醒时间、每周任务频…

即时战略游戏的AI策略思考

想起来第一次玩RTS游戏&#xff0c;就是框住一大群兵进攻&#xff0c;看他们把对面消灭干净……我接触的第一款游戏是《傲世三国》那会儿是小学&#xff0c;后来高中接触了魔兽地图编辑器&#xff0c;我发现自己喜欢直接看属性而省去争论和试验的步骤——我喜欢能一眼看透的感觉…