【力扣】剑指 Offer II 092. 翻转字符
题目


解题思路
一个很暴力的想法,在满足单调递增的前提下,使每一位分别取 1 或 0,去看看哪个结果小。
递归函数定义int dp(StringBuilder sb, int ind, int pre) sb是字符串,ind 是字符串当前位,pre 是字符串前一位(0或1)
dp函数表示:从字符串当前位ind开始到字符串结尾,这样一个子字符串,变为单调递增所需要翻转的最小次数。
因此题目所求就是 dp(sb, 0, 0)。第0位的前一位为0。
具体递归入口有四种情况,根据前一位是0或1 和 当前位是0或1来讨论,即
当前位为0:
前一位为0:
前一位为1:
当前位为1:
前一位为0:
前一位为1:
一开始递归函数定义不明确,导致无法形成重叠子问题,也就无法用备忘录来优化。
在考虑备忘录优化的时候,需要明确备忘录的每一维分别代表什么。参考了这位大佬的题解Java 递归+二维DP+空间优化。
代码
class Solution {
int[][] memo; // 备忘录
public int minFlipsMonoIncr(String s) {
int n = s.length();
memo = new int[n + 1][2];
for(int[] arr : memo){
Arrays.fill(arr, -1);
}
return dp(s.toCharArray(), 0, 0);
}
int dp(char[] s, int ind, int pre){ // sb是字符串,ind 是字符串当前位,pre 是前一位
if(ind == s.length){ // 到字符串结尾了,需要改变的字符为0,返回值为0
return 0;
}
if(memo[ind][pre] != -1){
return memo[ind][pre];
}
int res = 0;
if(s[ind] == '0'){ // 当前位为0
if(pre == 0){ // 前一位为 0
int a = dp(s, ind + 1, s[ind] - '0'); // 保持0不变
s[ind] = '1';
int b = dp(s, ind + 1, s[ind] - '0'); // 把当前0变为1,翻转次数加一
s[ind] = '0';
res += Math.min(a, b); // 取两者中最小的情况
}else if(pre == 1){ // 前一位为 1
s[ind] = '1';
res += dp(s, ind + 1, s[ind] - '0') + 1; // 前一位为1,当前位为0,必须变成1
s[ind] = '0';
}
}else if(s[ind] == '1'){ // 当前位为1
if(pre == 0){ // 前一位为 0
int a = dp(s, ind + 1, s[ind] - '0'); // 保持1不变
s[ind] = '0';
int b = dp(s, ind + 1, s[ind] - '0') + 1; // 把当前1变为0,翻转次数加一
s[ind] = '1';
res += Math.min(a, b); // 取两者中最小的情况
}else if(pre == 1){
res += dp(s, ind + 1, s[ind] - '0'); // 前一位为1,当前位为1,当前1必须保持不变
}
}
memo[ind][pre] = res;
return res;
}
}
优化后
class Solution {
int[][] memo; // 备忘录
public int minFlipsMonoIncr(String s) {
int n = s.length();
memo = new int[n + 1][2];
for(int[] arr : memo){
Arrays.fill(arr, -1);
}
return dp(s.toCharArray(), 0, 0);
}
int dp(char[] s, int ind, int pre){ // sb是字符串,ind 是字符串当前位,pre 是前一位
if(ind == s.length){ // 到字符串结尾了,需要改变的字符为0,返回值为0
return 0;
}
if(memo[ind][pre] != -1){
return memo[ind][pre];
}
int res = 0;
if(s[ind] == '0'){ // 当前位为0
if(pre == 0){ // 前一位为 0
// 保持0不变;
// 把当前0变为1,翻转次数加一;
// 取两者中较小的情况
res = Math.min(dp(s, ind + 1, 0), dp(s, ind + 1, 1) + 1);
}else if(pre == 1){ // 前一位为 1
res = dp(s, ind + 1, 1) + 1; // 前一位为1,当前位为0,必须变成1
}
}else if(s[ind] == '1'){ // 当前位为1
if(pre == 0){ // 前一位为 0
// 保持1不变;
// 把当前1变为0,翻转次数加一;
// 取两者中较小的情况
res = Math.min(dp(s, ind + 1, 1), dp(s, ind + 1, 0) + 1);
}else if(pre == 1){
res = dp(s, ind + 1, 1); // 前一位为1,当前位为1,当前1必须保持不变
}
}
memo[ind][pre] = res;
return res;
}
}
一开始写的时候有个问题,就是在递归函数的参数中记录结果,递归到边界的时候得到结果,这样就是一个纯递归的思路。并没有转成子问题的形式,因此我后续进行备忘录优化始终无法成功。原因还是递归函数定义有问题。
纯递归的代码
class Solution {
int[][][] memo;
public int minFlipsMonoIncr(String s) {
StringBuilder sb = new StringBuilder(s);
int n = s.length();
memo = new int[n + 1][2][2];
for(int[][] arr : memo){
for(int[] a : arr)
Arrays.fill(a, -1);
}
return dp(sb, 0, 0,0 ,sb.charAt(0) - '0');
}
int dp(StringBuilder sb, int ind, int cnt, int pre, int now){
if(ind == sb.length()){
return cnt;
}
if(memo[ind][pre][now] != -1){
return memo[ind][pre][now];
}
int res = 0;
if(sb.charAt(ind) == '0'){
if(ind - 1 >= 0){
if(sb.charAt(ind - 1) == '0'){
int a = dp(sb, ind + 1, cnt, sb.charAt(ind - 1) - '0',0);
sb.setCharAt(ind, '1');
int b = dp(sb, ind + 1, cnt + 1, sb.charAt(ind - 1) - '0',1);
sb.setCharAt(ind, '0');
res += Math.min(a, b);
}else{
sb.setCharAt(ind, '1');
res += dp(sb, ind + 1, cnt + 1, sb.charAt(ind - 1) - '0',1);
sb.setCharAt(ind, '0');
}
}else{
int a = dp(sb, ind + 1, cnt, 0,0);
sb.setCharAt(ind, '1');
int b = dp(sb, ind + 1, cnt + 1, 0,1);
sb.setCharAt(ind, '0');
res += Math.min(a, b);
}
}else{
if(ind - 1 >= 0){
if(sb.charAt(ind - 1) == '0'){
int a = dp(sb, ind + 1, cnt, sb.charAt(ind - 1) - '0',1);
sb.setCharAt(ind, '0');
int b = dp(sb, ind + 1, cnt + 1, sb.charAt(ind - 1) - '0',0);
sb.setCharAt(ind, '1');
res += Math.min(a, b);
}else{
res += dp(sb, ind + 1, cnt, sb.charAt(ind - 1) - '0',1);
}
}else{
int a = dp(sb, ind + 1, cnt, 0,1);
sb.setCharAt(ind, '0');
int b = dp(sb, ind + 1, cnt + 1, 0,0);
sb.setCharAt(ind, '1');
res += Math.min(a, b);
}
}
memo[ind][pre][now] = res;
return res;
}
}
其中cnt就是最后的结果,这样可以通过数据量小的问题,但数据量大的问题必定会超时,而且无法利用记忆化搜索优化。
总结
不管是不是动态规划问题,首先写出递归的暴力解。如果超时,考虑有没有重叠子问题,此时就要注意递归函数的定义,递归函数的返回值应该是子问题的解。可能一开始结果保存在函数参数中是比较好想的。如果一开始写的递归函数是结果在函数参数里的形式,要考虑将结果定义在返回值中,此时需要明确递归函数的定义。
【力扣】剑指 Offer II 092. 翻转字符的更多相关文章
- 刷题-力扣-剑指 Offer II 055. 二叉搜索树迭代器
剑指 Offer II 055. 二叉搜索树迭代器 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/kTOapQ 著作权归领扣网络所有 ...
- 力扣 - 剑指 Offer 58 - I. 翻转单词顺序
题目 剑指 Offer 58 - I. 翻转单词顺序 思路1 假如题目要求我们翻转字符串,那么我们可以从末尾往前开始遍历每一个字符,同时将每一个字符添加到临时空间,最后输出临时空间的数据就完成翻转了, ...
- 力扣 - 剑指 Offer 53 - II. 0~n-1中缺失的数字
题目 剑指 Offer 53 - II. 0-n-1中缺失的数字 思路1 排序数组找数字使用二分法 通过题目,我们可以得到一个规律: 如果数组的索引值和该位置的值相等,说明还未缺失数字 一旦不相等了, ...
- 力扣 - 剑指 Offer 57 - II. 和为s的连续正数序列
题目 剑指 Offer 57 - II. 和为s的连续正数序列 思路1(双指针/滑动窗口) 所谓滑动窗口,就是需要我们从一个序列中找到某些连续的子序列,我们可以使用两个for循环来遍历查找,但是未免效 ...
- 力扣 - 剑指 Offer 55 - II. 平衡二叉树
题目 剑指 Offer 55 - II. 平衡二叉树 思路1(后序遍历+剪枝) 这题是上一题剑指 Offer 55 - I. 二叉树的深度的进阶,逻辑代码和那个一样,也是后续遍历,获取两个子节点较大的 ...
- 刷题-力扣-剑指 Offer 15. 二进制中1的个数
剑指 Offer 15. 二进制中1的个数 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de- ...
- 刷题-力扣-剑指 Offer 42. 连续子数组的最大和
剑指 Offer 42. 连续子数组的最大和 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de ...
- 力扣 - 剑指 Offer 09. 用两个栈实现队列
目录 题目 思路 代码 复杂度分析 题目 剑指 Offer 09. 用两个栈实现队列 思路 刚开始想的是用stack1作为数据存储的地方,stack2用来作为辅助栈,如果添加元素直接push入stac ...
- 力扣 - 剑指 Offer 37. 序列化二叉树
目录 题目 思路 代码 复杂度分析 题目 剑指 Offer 37. 序列化二叉树 思路 序列化其实就是层序遍历 但是,要能反序列化的话,前.中.后.层序遍历是不够的,必须在序列化时候保存所有信息,这样 ...
随机推荐
- python读取文本数据某一列
import codecs f = codecs.open('test1 - 副本.txt', mode='r', encoding='utf-8') # 打开txt文件,以'utf-8'编码读取 l ...
- 26.ViewSet和action
在dispatch过程中,下列属性可用于 ViewSet : basename - 根url路径 action - 当前动作类型(例如 list , create ). detail - 用于指示 ...
- SQL--存储过程的使用
存储过程的概念 存储过程类似一个函数,可以执行一条或者多条SQL语句,可带参数,可返回值 为了简化操作,方便更改和扩展,将一个事件的处理封装在一个单元中供使用. 创建存储过程 --创建存储过程(不带参 ...
- JVM学习笔记——垃圾回收篇
JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...
- Angular SSR 探究
一般来说,普通的 Angular 应用是在 浏览器 中运行,在 DOM 中对页面进行渲染,并与用户进行交互.而 Angular Universal 是在 服务端 进行渲染(Server-Side Re ...
- C#使用MathNet库来对进行曲线拟合
下面是用来求取一条直线和一条拟合曲线交点的代码 /// <summary> /// 拟合曲线所筛选的点的个数 /// </summary> const int CurveNum ...
- zabbix-钉钉报警部署
zabbix-钉钉报警部署 1. 流程说明 申请钉钉机器人 获取Webhook配置安全设置 获取钉钉号 使用脚本(shell/python)调用钉钉接口: python 输入收件人 信息 配置发件人 ...
- Go语言核心36讲33
我们在前几次讲的互斥锁.条件变量和原子操作都是最基本重要的同步工具.在Go语言中,除了通道之外,它们也算是最为常用的并发安全工具了. 说到通道,不知道你想过没有,之前在一些场合下里,我们使用通道的方式 ...
- tp6 requset获取参数的方式
第一种:获取全部参数的值 request()->param() 1 第二种:获取排除某些字段的值,即获取其他值 request()->except(['serverToken','logi ...
- 关于cannot remove ‘directory': Directory not empty的解决办法
解决方法 首先你应该使用 rm -rf 目录名 这样确保可以递归删除目录 如果出现 cannot remove 'directory': Directory not empty 报错信息,重启电脑解决 ...