算法50:动态规划专练(力扣514题:自由之路-----4种写法)

题目: 力扣514 : 自由之路  . - 力扣(LeetCode)

题目的详细描述,直接打开力扣看就是了,下面说一下我对题目的理解:

事例1:

输入: ring = "godding", key = "gd"
输出: 4.

1. ring的第一个字符默认是指向12点方向的,这一点很重要

2. key的第一个字符为g,而ring中首字符和末尾字符都为g。因此,必然存在选择首字符的g还是末尾字符g的问题。

3. 即使ring中第一个字符为g,也还存在存在顺时针旋转和逆时针旋转的问题。(当然,当前案例比较极端,如果第一个字符不为g,理解起来更合适)

4. 这一题要求的是旋转的最小步数。因此,最终的值必然是获取最小值的。

5. 那么整体串起来分析:

        ring = "godding", key = "gd"

       

字符godding
下标0123456

a1. 如果ring的第一个g旋转,顺时针步数为0;逆时针步数为7;算上最终确认的1步,因此就               是在 1 和 8中选取最小值,选取1

a2. 接着a1继续分析,既然key的第一个字符已经确定了。那么key的第二个字符d又该选择                   了。我们到底是用下标为2的d呢,还是使用下标为3的d呢?

选择1: 下标为2的d,从a1的g顺时针旋转只需要2步,逆时针旋转需要5步,。算上确认的1步,就是在3和6中选取最小值,选取3

选择2: 下标为3的d, 从a1的g顺时针旋转只需要3步,逆时针旋转需要4步。算上确认的1步,就是在4和5中选取最小值. 选取 4

如果g使用的是ring中第一个字符,针对以上2种选择,最好的选择就是使用下标为2的d,顺时针旋转2步,即3步。  那么,总的代价就是 1 + 3 = 4。

开头,我们就说过,ring中有开头和结尾都存在g,因此存在选择的问题。

b1. 如果我们使用ring中下标为6的g最为key的开头。顺时针旋转需要1步,逆时针旋转需要6步,算上最终确认的1步,就是2步和7步。选取最小值2.

b2. 接着b1继续分析,既然key的第一个字符已经确定了。那么key的第二个字符d又该选择        了。我们到底是用下标为2的d呢,还是使用下标为3的d呢?

选择1: 下标为2的d,从b1的g顺时针旋转只需要4步,逆时针旋转需要3步,。算上确认的1步,就是在5和4中选取最小值,选取4

选择2: 下标为3的d, 从b1的g顺时针旋转只需要3步,逆时针旋转需要4步。算上确认的1步,就是在4和5中选取最小值. 选取4

如果g使用的是ring中最后一个字符,针对以上2种选择,最好都为4。  那么,总的代价就是 2 + 4 = 6。

最终,如果以ring中第一个g作为旋转选择,最小的步数为4;  以ring中最后一个g作为旋转选择,那么最小步数为6;  因此,当前案例最小步数为 Math.min(4, 6).

递归代码:

package 刷题.第三天;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 力扣514: 自由之路
 * https://leetcode.cn/problems/freedom-trail/
 */
public class C2_FreeDomTtail_leet514_递归 {

    //递归版本
    public int findRotateSteps(String ring, String key) {

        if (ring == null || ring.length() == 0 || key == null || key.length() == 0) {
            return 0;
        }

        char[] source = ring.toCharArray();
        char[] target = key.toCharArray();

        //记录下每个字符的位置,有可能存在重复值的情况
        HashMap<Character, List> map = new HashMap<Character, List>();
        for (int i = 0; i < ring.length(); i++) {
            if (map.containsKey(source[i])) {
                //放入下标的位置
                map.get(source[i]).add(i);
            }
            else {
                List list = new ArrayList();
                list.add(i);
                map.put(source[i], list);
            }
        }

        return process(map, source, 0, target, 0);
    }


    public int process (Map<Character, List> map,
                        char[] source, int sourceStartIndex,
                        char[] target, int targetIndex) {
        if (targetIndex == target.length) {
            return 0;
        }

        List<Integer> ops = map.get(target[targetIndex]);
        int minStep = Integer.MAX_VALUE;
        for (int i = 0; i < ops.size(); i++) {
            //从sourceStartIndex 到 ops.get(i) 最下步数; +1是确认按钮耗费的1步
            int step = getMinSteps(sourceStartIndex, ops.get(i), source.length) + 1;

            //深度优先遍历; 此时source的的开始位置为 ops.get(i)
            minStep = Math.min(minStep, step + process(map, source, ops.get(i), target, targetIndex + 1));
        }

        return minStep;
    }

    //获取从最小长度
    public int getMinSteps(int start, int end, int size)
    {
        //如果start < end, 则是顺时针; 反之, 逆时针
        int step1 = Math.abs(start - end);

        //如果step1是顺时针,那么step则是逆时针; 反之,顺时针
        int step2 = size - step1;

        return Math.min(step1, step2);
    }

    public static void main(String[] args) {
        C2_FreeDomTtail_leet514_递归 ss = new C2_FreeDomTtail_leet514_递归();
        String source = "godding";
        String target = "gd";

        System.out.println(ss.findRotateSteps(source, target));
    }
}

测试结果:

超时是好事情,说明整体逻辑大概率是没有问题的。超时,说明递归计算的次数有问题。上方的分析过程中,a2和b2 都是针对d进行逻辑判断的,明显存在重复的过程。那么,就需要在递归的基础之上添加缓存了,俗称记忆化搜索。

我之前在说动态规划的时候就说过,如果表结构依赖不严格,或者说即使依赖严格表结构,但是没有优化的空间。  递归 + 缓存 == 动态规划。

递归+缓存版本:

package 刷题.第三天;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 力扣514: 自由之路
 * https://leetcode.cn/problems/freedom-trail/
 */
public class C2_FreeDomTtail_leet514_递归_缓存 {

    //递归 + 缓存
    public int findRotateSteps(String ring, String key) {

        if (ring == null || ring.length() == 0 || key == null || key.length() == 0) {
            return 0;
        }

        char[] source = ring.toCharArray();
        char[] target = key.toCharArray();

        //记录下每个字符的位置,有可能存在重复值的情况
        HashMap<Character, List> map = new HashMap<Character, List>();
        for (int i = 0; i < ring.length(); i++) {
            if (map.containsKey(source[i])) {
                //放入下标的位置
                map.get(source[i]).add(i);
            }
            else {
                List list = new ArrayList();
                list.add(i);
                map.put(source[i], list);
            }
        }

        int[][] dp = new int[source.length][target.length];
        for (int i = 0; i < source.length; i++) {
            for (int j = 0; j < target.length; j++) {
                //代表没算过
                dp[i][j] = -1;
            }
        }

        return process(map, source, 0, target, 0, dp);
    }


    public int process (Map<Character, List> map,
                        char[] source, int sourceStartIndex,
                        char[] target, int targetIndex,
                        int[][] dp) {

        if (targetIndex == target.length) {
            return 0;
        }

        //缓存
        if (dp[sourceStartIndex][targetIndex] != -1) {
            return dp[sourceStartIndex][targetIndex];
        }

        List<Integer> ops = map.get(target[targetIndex]);
        int minStep = Integer.MAX_VALUE;
        for (int i = 0; i < ops.size(); i++) {
            //从sourceStartIndex 到 ops.get(i) 最下步数; +1是确认按钮耗费的1步
            int step = getMinSteps(sourceStartIndex, ops.get(i), source.length) + 1;

            //深度优先遍历; 此时source的的开始位置为 ops.get(i)
            minStep = Math.min(minStep, step + process(map, source, ops.get(i), target, targetIndex + 1, dp));

            dp[sourceStartIndex][targetIndex] = minStep;
        }

        return minStep;
    }

    //获取从最小长度
    public int getMinSteps(int start, int end, int size)
    {
        //如果start < end, 则是顺时针; 反之, 逆时针
        int step1 = Math.abs(start - end);

        //如果step1是顺时针,那么step则是逆时针; 反之,顺时针
        int step2 = size - step1;

        return Math.min(step1, step2);
    }

    public static void main(String[] args) {
        C2_FreeDomTtail_leet514_递归_缓存 ss = new C2_FreeDomTtail_leet514_递归_缓存();
        String source = "godding";
        String target = "gd";

        System.out.println(ss.findRotateSteps(source, target));
    }
}

测试结果:

84%的胜率,8毫秒,已经相当优秀了。其实,这就是最优解

第三版本:纯动态规划

动态规划,那就需要分析递归的逻辑了。下面以ring作为行,以key作为列

第一步:

0 (g)1 (d)
0 (g)

下标0的g: 

顺时针:1步

逆时针:8步

选取1步

1 (o)
2 (d)

3 (d)

4 (i)
5 (n)
6 (g)

下标6的g :

顺时针:2步

逆时针:7步

选取2步

第二步:

0 (g)1 (d)
0 (g)

下标0的g: 1步

1 (o)
2 (d)

下标为2的d:

从下标为0的g过来,

顺时针2步,逆时针5步,

算上确认的1步,

就是3步和6步,选取小值,即3步

从下标为6的g过来,

顺时针3步,逆时针4步,

算上确认的1步,

就是4步和5步,选取小值,即4步

最终值就是:

1+3 和 2 +4选取小的。即4步

1是下标为0的g耗费的1步

2是下标为6的g耗费的2步

3 (d)

下标为3的d:

从下标为0的g过来,

顺时针3步,逆时针4步,

算上确认的1步,

就是4步和5步,选取小值,即4步

从下标为6的g过来,

顺时针4步,逆时针3步,

算上确认的1步,

就是5步和4步,选取小值,即4步

最终值就是:

1+4 和 2 +4选取小的。即5步

1是下标为0的g耗费的1步

2是下标为6的g耗费的2步

4 (i)
5 (n)
6 (g)下标6的g : 2步

因此,最终最小的步数就是当key遍历完最后一个字符得到,即 1 + 3 = 4步;

纯动态规划

package 刷题.第三天;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 力扣514: 自由之路
 * https://leetcode.cn/problems/freedom-trail/
 */
public class C2_FreeDomTtail_leet514_动态规划 {

    //纯动态规划
    public int findRotateSteps(String ring, String key) {

        if (ring == null || ring.length() == 0 || key == null || key.length() == 0) {
            return 0;
        }

        char[] source = ring.toCharArray();
        char[] target = key.toCharArray();

        //记录下每个字符的位置,有可能存在重复值的情况
        HashMap<Character, List> map = new HashMap<Character, List>();
        for (int i = 0; i < ring.length(); i++) {
            if (map.containsKey(source[i])) {
                //放入下标的位置
                map.get(source[i]).add(i);
            }
            else {
                List list = new ArrayList();
                list.add(i);
                map.put(source[i], list);
            }
        }

        int[][] dp = new int[source.length][target.length + 1];
        //最终返回的最小值
        int finalMinStep = Integer.MAX_VALUE;
        //第一列
        List<Integer> ops = map.get(target[0]);
        for (int index : ops) {
            dp[index][0] = getMinSteps(0, index, source.length) + 1;
            //如果要拼接的key只有一个字符,直接获取最小值即可
            finalMinStep = Math.min(finalMinStep,  dp[index][0]);
        }

        //如果要拼接的字符长度超过1,那么finalMinStep的值需要
        //等到 target的最后一列才能确定
        if (target.length > 1) {
            finalMinStep = Integer.MAX_VALUE;
        }
        //列遍历,从第二列开始往后遍历
        for (int i = 1; i < target.length ; i++)
        {
            //当前列对应的行信息
            List<Integer> ops2 = map.get(target[i]);
            //当前列前一列对应的行信息
            List<Integer> ops3 = map.get(target[i-1]);

            for (int j : ops2)  //结束
            {
                //j行i列的默认最小值
                int minStep = Integer.MAX_VALUE;
                for(int m : ops3) //开始
                {
                    //从m行到j行的步数
                    int curStep = getMinSteps(m, j, source.length) + 1;
                    //dp[m][i-1] : 从0到m累计步数
                    //dp[j][i-1] + curStep : 代表从0行到j行累计步数
                    int steps = dp[m][i-1] + curStep;
                    //更新j行i列的最小值
                    minStep = Math.min(minStep, steps);
                    dp[j][i] = minStep;
                }

                //要拼接字符串的最后一个字符,也就是说可以
                //已经全部拼接完了
                if (i == target.length - 1) {
                    finalMinStep = Math.min(finalMinStep, dp[j][i]);
                }
            }
        }

        return finalMinStep;
    }


    //获取从最小长度
    public int getMinSteps(int start, int end, int size)
    {
        //如果start < end, 则是顺时针; 反之, 逆时针
        int step1 = Math.abs(start - end);

        //如果step1是顺时针,那么step则是逆时针; 反之,顺时针
        int step2 = size - step1;

        return Math.min(step1, step2);
    }

    public static void main(String[] args) {
        C2_FreeDomTtail_leet514_动态规划 ss = new C2_FreeDomTtail_leet514_动态规划();
        /*String source = "godding";
        String target = "gd";*/

        String source = "eh";
        String target = "h";

        System.out.println(ss.findRotateSteps(source, target));
    }
}

测试结果:

10毫秒,76%胜率,也还行。

第四种解法,即官方解法。因为胜率没有我写的两个版本的高,我就不说了。

官方代码胜率:

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

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

相关文章

强化学习:技术创新与应用实践

目录 前言1 强化学习原理和分类1.1 强化学习的原理1.2 基于值函数的方法1.3 基于策略的方法1.4 深度强化学习 2 强化学习应用2.1 游戏领域2.2 机器人控制2.3 金融交易 3 未来展望结语 前言 强化学习&#xff08;Reinforcement Learning&#xff09;作为人工智能领域的重要分支…

【Unity】进度条和血条的三种做法

前言 在使用Unity开发的时候&#xff0c;进度条和血条是必不可少的&#xff0c;本篇文章将简单介绍一下几种血条的制作方法。 1.使用Slider Slider组件由两部分组成&#xff1a;滑动区域和滑块。滑动区域用于显示滑动条的背景&#xff0c;而滑块则表示当前的数值位置。用户可…

数据结构:静态链表(编程技巧)

链表的元素用数组存储&#xff0c; 用数组的下标模拟指针。 一、理解 如果有些程序设计语言没有指针类型&#xff0c;如何实现链表&#xff1f; 在使用指针类型实现链表时&#xff0c;我们很容易就可以直接在内存中新建一块地址用于创建下一个结点&#xff0c;在逻辑上&#x…

OpenCV 将rgb图像转化成字符图像

将RGB图像转换成字符图像&#xff08;ASCII art&#xff09;通常涉及到灰度化、降采样、映射字符等一系列步骤。以下是一个简化的OpenCVC实现示例&#xff1a; #include <opencv2/opencv.hpp> #include <iostream> #include <string>// 字符映射表&#xff…

R语言绘制散点密度图ggdentity

使用R语言绘制二维密度图 下图是一张常见的二维核密度散点图&#xff0c;能够清晰直观的反映出数据之间的分布趋势&#xff0c;颜色越红的位置数据越集中分布。今天分享的笔记是在R语言中绘制该图的两种常见方法&#xff0c;提供过程代码。 论文中常见的这种展示两组数据之间分…

嵌入式驱动学习第三周——container_of()宏

前言 Linux内核编程中&#xff0c;会经常看见一个宏函数container_of&#xff0c;那么这究竟是什么呢&#xff0c;本篇博客记录学习container_of的过程。 嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程&#xff0c;未来预计四个月将高强度更新本专栏&#xff0c;喜欢的可…

【C++】stack、queue模拟实现+仿函数

stack、queue模拟实现仿函数 stack定义stack模拟实现 queue定义queue模拟实现 priority_queue定义priority_queue模拟实现 deque定义底层分析 容器适配器定义种类 仿函数控制类里面数据的比较逻辑回调函数仿函数两者区别 铁汁们&#xff0c;今天给大家分享一篇stack、queue模拟…

Vue+SpringBoot打造服装店库存管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 角色管理模块2.3 服装档案模块2.4 服装入库模块2.5 服装出库模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 角色表3.2.2 服装档案表3.2.3 服装入库表3.2.4 服装出库表 四、系统展示五、核心代码5.…

算法打卡day17|二叉树篇06|Leetcode 654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

算法题 Leetcode 654.最大二叉树 题目链接:654.最大二叉树 大佬视频讲解&#xff1a;最大二叉树视频讲解 个人思路 大概思路就是在数组中 找最大值的节点作为当前节点&#xff0c;用最大值的index切割左右子树的区间&#xff0c;往复循环到数组元素为0&#xff1b; 解法 递…

【数据结构】二叉搜索树底层刨析

文章目录 1. 二叉搜索树的实现2. 二叉搜索树的应用3. 改造二叉搜索树为 KV 结构4. 二叉搜索树的性能分析 1. 二叉搜索树的实现 namespace key {template<class K>struct BSTreeNode{typedef BSTreeNode<K> Node;Node* _left;Node* _right;K _key;BSTreeNode(const…

【1】Python零基础起步

什么是编程(Programming) 编程是编定程序的中文简称&#xff0c;就是让计算机代码解决某个问题&#xff08;目的&#xff09;&#xff0c;对某个计算体系规定一定的运算方式&#xff0c;使计算体系按照该计算方式运行&#xff0c;并最终得到相应结果的过程&#xff08;手段&am…

Java Day 10 io流

IO流 1、前置知识 字符集1.1 标准ASCII1.2 GBK编码1.3 UTF-321.4 UTF-81.5 编码和解码方法 2、IO流2.1 流的分类2.2 FileInputStream2.2.1 常用方法 2.3 FileOutputStram2.3.1 常用方法2.3.2 文件复制案例 2.4 释放资源的方式2.4.1 try-catch-finally2.4.2 try-with-resource 1…

ftp和fxp哪个传传输快,传输大文件该怎么选择?

在当今数字化时代&#xff0c;大文件传输已成为日常工作和商业活动中不可或缺的一部分。无论是跨国公司的数据交换&#xff0c;还是个人用户的大型媒体文件分享&#xff0c;选择一个高效的传输协议至关重要。FTP和FXP是两种常用的文件传输方式&#xff0c;但在传输大文件时&…

nginx gzip性能优化 —— 筑梦之路

对比使用和不使用gzip static处理 1. 不使用 gzip static 时的 gzip 处理 如果你不使用 gzip_static 而只是 "gzip on"&#xff0c;它每次都会被压缩并发送。 虽然它实际上可能缓存在内存中&#xff0c;但传统观点是 "每次都会执行压缩处理&#xff0c;因此 CP…

【SRE系列之docker容器】--dockerfile镜像优化

dockerfile镜像优化 1.1 镜像优化方法 系统镜像采用ubuntu或者alpine&#xff0c;会比centos少1G左右编写业务镜像时从官网拉取镜像&#xff0c;其余配置根据业务需求再配置编写dockerfile时把不用的安装包卸载或者删除尽量减少run命令的使用&#xff08;一个run命令&#xf…

【兆易创新GD32H759I-EVAL开发板】图像处理加速器(IPA)的应用

GD32H7系列的IPA&#xff08;Image Pixel Accelerator&#xff09;是一个高效的图像处理硬件加速器&#xff0c;专门设计用于加速图像处理操作&#xff0c;如像素格式转换、图像旋转、缩放等。它的优势在于能够利用硬件加速来实现这些操作&#xff0c;相比于软件实现&#xff0…

YOLOv9实例分割教程|(二)验证教程

专栏地址&#xff1a;目前售价售价59.9&#xff0c;改进点30个 专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 一、验证 打开分割验证文件&#xff0c;填入数据集配置文件、训练好的权重文件&…

go语言基础笔记

1.基本类型 1.1. 基本类型 bool int: int8, int16, int32(rune), int64 uint: uint8(byte), uint16, uint32, uint64 float32, float64 string 复数&#xff1a;complex64, complex128 复数有实部和虚部&#xff0c;complex64的实部和虚部为32位&#xff0c;complex128的实部…

yocto是个什么东东

yocto不是个什么东东 在我们了解Yocto项目是什么之前&#xff0c;让我们先了解一下它不是什么。 Yocto项目不是用于现有硬件的软件开发工具包&#xff08;SDK&#xff09;&#xff0c;而是用于构建这样一个工具包。 Yocto项目不是可以部署到硬件上的系统二进制镜像&#xff…

客服销冠偷偷用的提效神器!无广很实用

近期发现我的同事每天上班必登录的一款软件——客服宝聊天助手&#xff0c;用过才发现&#xff1a;真客服办公的提效神器&#xff01;感兴趣的小伙伴请往下看~一、客服宝的简介&#xff1a;客服宝聊天助手&#xff0c;是一款跨平台快捷回复工具。自带多种功能&#xff0c;有效帮…