60题学会动态规划系列:动态规划算法第一讲

news2025/1/9 16:50:41

坚持就是胜利 - - 

文章目录

1.第N个泰波那切数

2.三步问题

3.使用最小花费爬楼梯

4.解码方法


1.第N个泰波那切数

力扣链接:力扣

泰波那契序列 Tn 定义如下: 

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

 首先我们分析一下题目,在n>=0的条件下也就是说都是正数,然后给了一个算泰博纳妾数的公式,这道题很简单,但是我们还是一步步带着大家来分析:

首先在我们以后做动态规划的题,都按照一下这个模板来编写:

1.状态表示

首先我们先把公式重新推导一下,让每边减3得到Tn = Tn-3 + Tn-2 + Tn-1 ,Tn就是第n个泰博纳妾数,根据前三个泰波纳妾数推导出来的第n个,所以说dp[i]的状态就是第i个泰波纳妾数

2.状态转移方程

dp[i] = dp[i-1] + dp[i-2] + dp[i-3]    因为第i个泰波纳妾数等于前三个泰波纳妾数的和,所以转移方程就是这个

3.初始化

我们初始化的目的是为了防止越界,如下图:

第0个泰波那切数的前三个都是非法的,所以我们必须给定第0个泰波纳妾数,同理dp[1]和dp[2]同样会越界,而题目又贴心的给了我们初始值,dp[0] = 0,dp[1] = 1,dp[2] = 1 

4.填表以及确定填表顺序

因为我们是知道左边的值,依靠左边的值推出来右边的值,所以顺序是从左向右

5.返回值

既然让我们返回第n个泰波纳妾数,而我们的状态表示dp[i]就表示第i个泰波纳妾数,所以返回dp[n]就是第n个泰波纳妾数

思路理解了我们直接写代码:

class Solution {
public:
    int tribonacci(int n) {
        //1.创建dp表
        //因为是从0开始的所以要多开一个位置
        vector<int> dp(n+1);
        //处理边界条件
        if (n==0) return 0;
        if (n==1||n==2) return 1;
        //2.初始化
        dp[0] = 0,dp[1] = dp[2] = 1;
        //3.状态转移方程
        for (int i = 3;i<=n;i++)
        {
            dp[i] = dp[i-1]+dp[i-2]+dp[i-3];
        }
        //返回值
        return dp[n];
    }
};

 首先创建dp表,由于是从0开始的所以要多开一个位置存储第n个泰波纳妾数,所以空间大小为n+1,因为第0个,第1个,第2个泰波那切数都给出来了,所以我们只需要从第3个泰波那切数开始计算,然后我们处理边界条件,如果不处理那么数组会直接越界,最后返回dp[n]即可。 

下面我们再讲解一下用滚动数组优化的方式,在上面的代码中我们要求第n个太波那契数列数是没必要将n之前的所有泰波那契数都保存的,我们只需要保存前三个就可以算出来了,所以我们只需要用四个变量就能解决这个问题:

 一开始d是第3个泰波那契数,经过滚动后下一次d变成了第4个泰波那契数,下面我们实现一下代码:

class Solution {
public:
    int tribonacci(int n) {
        //初始化前3个泰波那契数
        int a = 0;
        int b = 1;
        int c = 1;
        int d = 0;
        if (n==0) return 0;
        if (n==1||n==2) return 1;
        for (int i = 3;i<=n;i++)
        {
            d= a+b+c;
            a = b;
            b = c;
            c = d;
        }
        return d;
    }
};

 需要注意的是:滚动的时候应该是a向d去依次赋值,不能反过来反过来会导致原本c的值被覆盖。

2.三步问题

力扣链接:力扣

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。

 首先题目说有n阶台阶,小孩一次可以上1阶或者2阶或者3阶,然后求有多少种上楼的方式,然后还说了题目结果会很大需要取模,下面我们就演示一下上楼梯的步骤:

 首先从地平线到第一个台阶只有一种方法,所以第一个台阶的方法数就是1.

到达第二个台阶可以从第0个台阶跨越2步到达(这是一种方法),也可以从第一个台阶跨越1步到达,由于要想从第一个台阶到第二个台阶就得先到第一个台阶,而到达第一个台阶的方法数是1,所以从第一个台阶到达第二个台阶的方法数为1*1==1.所以到达第二个台阶的总次数为2

到达第三个台阶可以直接从第0个台阶跨越3步到达,也可以从第一个台阶跨越2步到,还可以从第二个台阶跨越1步到达。首先从第0个台阶跨越3步到达方法数是1,从第一个台阶跨越需要知道到达第一个台阶的方法数所以是1*1==1,从第二个台阶跨越1步到达首先要计算到达第二个台阶的方法数所以是2*1 == 2,所以总次数为1 + 1 + 2 = 4.

到达第4个台阶可以从第一个台阶直接跨越3步,也可以从第二个台阶跨越2步,也可以从第3个台阶跨越一步。第一个台阶直接跨越3步的方法数为:1*1==1,第二个台阶跨越2步的方法数为2*1==2,第3个台阶跨越一步的方法数为4*1==4,所以总方法数为1+2+4==7

下面我们直接套模板:

1.状态表示

我们可以看到如果是第5个台阶的话那么有13种方法,而我们创建一个dp表将每个台阶的方法数放入,如下图:

所以dp[i]就表示第i个台阶的方法数。 

2.状态转移方程

可以通过图看到,第5个台阶的方法数是前3个台阶的方法数之和,所以状态转移方程为:

dp[i] = dp[i-1]  + dp[i-2] + dp[i-3]

3.初始化

由于第0,1,2个台阶会越界,所以我们直接给出方法数:dp[0] = 1,dp[1] = 1,dp[2] = 2,这里可能有人会有疑问,为什么到达地平线还有1个方法呢?在这里我们只能说要算第3个台阶的方法数dp[0]必须给1,这是靠分析题目得出来的。

4.填表

从左向右填表

5.返回值

dp[i]表示第i个台阶的方法数,所以返回dp[n]即可。

class Solution {
public:
    int waysToStep(int n) {
       const int MOD = 1e9+7;
       //创建dp表
       vector<int> dp(n+1);
       //解决边界问题
       if (n==0||n==1) return 1;
       if (n==2) return 2;
       dp[0] = 1,dp[1] = 1,dp[2] = 2;
       for (int i = 3;i<=n;i++)
       {
           dp[i] = ((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD;
       }
       return dp[n];  
    } 
};

本题需要注意的是要对每次求出的台阶数取模,否则就会越界,我们直接用变量保存将要取模的数

然后在计算结果时每次相加就取模一次。

3.使用最小花费爬楼梯

力扣链接:力扣

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

 这道题有一个隐含条件,直接看实例1,我们大多数人都会认为楼顶就是数组的最后一个位置也就是20的位置,如果是20那么答案应该是10才对,为什么是15呢?因为楼顶其实是在数组最后一个位置的下一个位置:

了解了这个后我们讲一下如何计算:

首先能到达顶楼的位置有两个,要不然是顶楼-1的位置,要不然就是顶楼-2的位置,因为题目已经告诉了每次只能爬一个台阶或者两个台阶。所以我们只需要求出顶楼-1位置的最小花费和顶楼-2位置的最小花费的较小值,我们以实例1为例:

 由于题目已经告诉从0或1开始爬楼梯,所以到达0位置和1位置的最小花费是0.15是第一个台阶到达第一个台阶最小花费是0,然后15跨两步到楼顶需要支付15,所以15->楼顶的最小花费是15.

要计算20这个台阶的最小花费就要先知道哪两个台阶能到20这个位置,很明显从第0个台阶跨两步到20,到达第0个台阶的花费为0,从第0个台阶到第2个台阶的花费为10,所以10->20的花费为10,同理15->20的花费为15,那么到台阶2的最小花费就是10,然后从20->楼顶的花费是20,所以20->楼顶的最小花费是10+20==30。

取楼顶-1台阶的最小值和楼顶-2台阶的最小值相比较,最小的就是到楼顶的最小值,所以刚刚的实例2答案为15.

1.状态表示

dp[i]表示到达第i个台阶的最小花费

2.状态转移方程

dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

3.初始化

因为题目告诉了从第0个台阶或者第1个台阶开始爬,所以之前到达0或1台阶的最小花费是0,所以dp[0]和dp[1]都等于0

4.填表

5.返回值

由于数组是从0开始的,size()的大小就是楼顶的位置,所以返回dp[size()]即可。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        if (cost.size()<=0)
        {
            return 0;
        }
        //创建dp表
        int sz = cost.size();
        vector<int> dp(sz+1);
        //初始化
        dp[0] = dp[1] = 0;
        for (int i = 2;i<=sz;i++)
        {
            dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[sz];
    }
};

 上面我们是按照从前往后推的下面我们也可以换一个思想,从后往前推:

 比如我们现在在第7个台阶,那么我们支付了第7个台阶的费用后可以向后走一步也可以走两步,走一步到第8个台阶,第8个台阶花费100直接能到楼顶。走两步到第9个台阶,花费1直接就能到楼顶,这个时候我们第7个台阶到达楼顶的最小花费就是:第8个台阶和第9个台阶到达楼顶的最小花费的最小值,所以第7个台阶的最小花费就是1+1(+1是因为自己支付了费用要向后跳),以此类推从第7个台阶推到第0个台阶,又因为我们是从0或1出发,所以最后的返回值是0位置和1位置最小花费的最小值。

1.状态表示

dp[i]表示从i位置支付费用后到达楼顶花费的最小值

2.状态转移方程

dp[i] = min(dp[i+1],dp[i+2]) + cost[i]   (要记住我们从i位置起跳是需要花钱的所以要加上cost[i])

3.初始化

可以看到我们的转移方程是i+1和i+2,所以在距离楼顶的2个位置和1个位置的台阶是无法计算的因为越界了,并且从这两个位置起跳到楼顶只需要花费自身cost的费用。所以dp[楼顶-1] = cost[楼顶-1],dp[楼顶-2] = cost[楼顶-2]

4.填表

从右向左填表:

5.返回值

返回dp[0]和dp[1]的较小值

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        //创建dp表
        int sz = cost.size();
        vector<int> dp(sz);
        //初始化
        dp[sz-1] = cost[sz-1];
        dp[sz-2] = cost[sz-2];
        for (int i = sz-3;i>=0;i--)
        {
            dp[i] = min(dp[i+1],dp[i+2]) + cost[i];
        }
        return min(dp[0],dp[1]);
    }
};

4.解码方法

力扣链接:力扣

 首先我们要能读懂题,我们做的是解码工作,题目会给我们一个数字字符串,然后我们可以有多少种方法将数字转化为不同的字符顺序,要注意的是一旦有0作为前导就会解码失败,可以看实例3。(但是如果是两个字符,比如10那么可以解码为K那么就是一种方法).首先题目要求字符串的解码总数,那我们根据经验就可以得出状态表示,解码总数就是以字符串最后一个字符为结尾的总解码数。

1.状态表示

dp[i]表示以i字符为结尾的总解码数

2.状态转移方程

首先我们要知道dp[i]的方法数就要看s[i]这个字符能否解码,如果这个字符>='1'并且<='9'那么就是可以解码的,一旦这个字符可以解码就说明总方法数是dp[i-1],如下图:

 比如上图中dp[i]有三种方法分别是abc,acb,cba,当s[i]可以解码时就可以放在他们的后面,但是方法还是3种,因为我们要求的是以i为结尾的方法数而不是字符个数!当s[i]为0时那么在前面的所有努力都白费了,直接dp[i]等于0

当我们的s[i]可以和s[i-1]相结合变成一个新的字符时,那么就可以把s[i]和s[i-1]看成一个字符,那么总方法数就是前面i-2的方法数,所以是dp[i-2]

3.初始化

状态转移方程中只有0和1位置会越界,所以初始化0和1

首先如果第一个字符是>='1'&&<='9',那么dp[0]就是1,否则就是0

如果第2个字符可以解码,那么方法数就是i-1的方法数那就是1,如果第二个字符还可以和第一个字符结合,那么方法数就变成2

4.填表

已知0和1位置,所以从左向右填表:

5.返回值

返回以字符串最后一个字符为结尾的总方法数

class Solution {
public:
    int numDecodings(string s) {
      //创建dp表
      int sz = s.size();
      vector<int> dp(sz,0);
      //初始化
      dp[0] = s[0]!='0';
      //解决边界条件
      if (dp[0]==0) return 0;
      if (sz==1) return dp[0];
      //初始化
      if (s[1]>='1'&&s[1]<='9')
      {
          dp[1] = 1;
      }
      int t = (s[0]-'0')*10 + s[1]-'0';
      if (t>=10&&t<=26)
      {
          ++dp[1];
      }
      for (int i = 2;i<sz;i++)
      {
          if (s[i]>='1'&&s[i]<='9')
          {
              dp[i] += dp[i-1];
          }
          int t = (s[i-1]-'0')*10+s[i]-'0';
          if (t>=10&&t<=26)
          {
              dp[i] += dp[i-2];
          }
      }
      return dp[sz-1];
    }
};

 需要注意的是如果s[0]为0那么是不能解码的,当字符串长度为1时要返回dp[0]也就是第一个字符的方法数,否则我们下面的初始化dp[1]会越界。

下面我们讲一下能优化初始化的思路:

我们可以给dp表多开一个空间,这个位置放的值要视情况而定,如下图:

 我们在原先dp表的基础上多开了一个位置,那么这个位置该放多少呢?首先我们知道,第一个字符是一定要初始化的,第一个字符对应的dp表的位置再dp[1],我们将这个位置初始化使用动态转移方程的时候才不会越界(只要满足i-1,i-2不越界),dp[1]是我们自己初始化的,dp[2]需要状态转移方程计算,当第二个字符可以解码那么方法数是dp[i-1]也就是第一个字符的方法数1,然后第二个字符还可以和第一个字符结合,一旦结合解码数就变成dp[i-2],而dp[i-2]是我们的虚拟位置,如果我们第一个虚拟位置初始化为0,计算dp[i-2]的时候就会少一个方法数,所以1才是正确的,我们也可以再举一个例子:

 拿"2 0"来说,0位置的方法数是dp[i-1]+dp[i-2],由于是0所以dp[i-1]是0,dp[i-2]是我们的虚拟位置,2和0是可以结合的所以要有一种方法,如果虚拟位置为0就是0种方法就错了,所以虚拟位置应该为1.有了虚拟位置那么计算对应的字符串都需要-1,原来-1的要变成-2的字符,比如dp[i]的方法数是s[i-1]位置的字符的方法数。

class Solution {
public:
    int numDecodings(string s) {
      //创建dp表
      int sz = s.size();
      vector<int> dp(sz+1,0);
      //初始化
      dp[0] = 1;
      dp[1] = s[0]!='0';
      for (int i = 2;i<=sz;i++)
      {
          if (s[i-1]>='1'&&s[i-1]<='9')
          {
              dp[i] += dp[i-1];
          }
          int t = (s[i-2]-'0')*10+s[i-1]-'0';
          if (t>=10&&t<=26)
          {
              dp[i] += dp[i-2];
          }
      }
      return dp[sz];
    }
};

可以看到此方法不仅简化了代码而且让我们的初始化变的更简单。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多线程 -- 线程安全问题(3)

本篇重点: 总结线程安全问题的原因以及解决办法 目录 synchronized 加锁关键字join 和 synchronized 的区别volatile 关键字 在上一篇中我们介绍了Thread类的基本使用方法, 本篇将会介绍有关于线程的安全问题 线程不安全的原因: 抢占式执行(罪魁祸首, 万恶之源) 多个线程修改同…

搜索推荐系统[10]项目实战系列Z5:汽车说明书跨模态智能问答系统,针对汽车说明书(可自定义文档)进行自动问答,采用了OCR、RocketQA等技术

搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术细节以及项目实战(含码源) 专栏详细介绍:搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术…

小程序之页面通信派发通知

文章目录 1. 介绍小程序页面通信的概念解释小程序页面通信的意义和必要性介绍小程序页面通信的方法 2. 小程序页面通信的实现示例通过事件传递数据实现页面之间通信通过全局变量实现页面之间通信 3. 实现小程序页面之间的消息通知介绍小程序发布订阅模式的概念使用事件订阅-发布…

网络通信IO模型-BIO

承接上文网络通信IO模型上 BIO的Java代码 服务端创建一个ServerSocket&#xff0c;绑定了端口号8090&#xff0c;目的是让客户端和服务端建立连接后进行通信&#xff0c;然后进入死循环&#xff0c;死循环里面会调用server.accept得到一个socket客户端&#xff0c;打印客户端的…

【PyQt5】指示灯显示

【PyQt5】指示灯显示 1、背景2、代码示例3、QtDesigner绘制 1、背景 利用Qt5写工业控制软件交互界面的时候&#xff0c;经常需要在界面上有指示灯功能。 例如下面的明暗表示串行端口的连接和断开。 我们本质是用Qt5的label文本标签来实现的&#xff0c;即通过设置标签的样式表…

115.删除有序数组中的重复项 removeDuplicatesFromSortedArray

文章目录 题目描述解题思路代码详解运行截图 题目描述 题目链接 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元…

C# | 凸包算法之Jarvis,寻找一组点的边界/轮廓

C#实现凸包算法之Jarvis 文章目录 C#实现凸包算法之Jarvis前言示例代码实现思路测试结果结束语 前言 这篇关于凸包算法的文章&#xff0c;本文使用C#和Jarvis算法来实现凸包算法。 首先消除两个最基本的问题&#xff1a; 什么是凸包呢&#xff1f; 凸包是一个包围一组点的凸多…

驱动LSM6DS3TR-C实现高效运动检测与数据采集(1)----获取ID

概述 本文将介绍如何驱动和利用LSM6DS3TR-C传感器&#xff0c;实现精确的运动感应功能。LSM6DS3TR-C是一款先进的6轴惯性测量单元&#xff08;IMU&#xff09;&#xff0c;集成了三轴加速度计和三轴陀螺仪&#xff0c;可用于测量和检测设备的加速度、姿态和运动。 本文将提供L…

车载软件架构 —— 闲聊几句AUTOSAR OS(二)

我是穿拖鞋的汉子,魔都中坚持长期主义的工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 在最艰难的时候,自己就别去幻想太远的将来,只要鼓励自己过好今天就行了! 这世间有太多的猝不及防,有些东西根本不配占有自己的情绪,人生就是一场体验,…

牛客HJ43迷宫问题 - 创建智能体通过策略自己找路

文章目录 问题描述思路代码C 问题描述 描述 定义一个二维数组 N*M &#xff0c;如 5 5 数组下所示&#xff1a; int maze[5][5] { 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; 它表示一个迷宫&#xff0c;其中的1表示墙壁&#xff0…

SPA首屏加载速度慢的怎么解决?

SPA首屏加载速度慢的怎么解决&#xff1f; 加载慢的原因 网络延时问题资源文件体积是否过大资源是否重复发送请求去加载了加载脚本的时候&#xff0c;渲染内容堵塞了 解决方案 1.减小入口文件体积 常用的手段是路由懒加载&#xff0c;把不同路由对应的组件分割成不同的代码…

如何在华为OD机试中获得满分?Java实现【水仙花数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 所谓水仙花数,是指一个…

LeetCode高频算法刷题记录10

文章目录 1. 旋转图像【中等】1.1 题目描述1.2 解题思路1.3 代码实现 2. 组合总和【中等】2.1 题目描述2.2 解题思路2.3 代码实现 3. 回文链表【简单】3.1 题目描述3.2 解题思路3.3 代码实现 4. 字符串解码【中等】4.1 题目描述4.2 解题思路4.3 代码实现 5. 多数元素【简单】5.…

高压功率放大器ATA4014VS高压功率放大器HSA42014

高压功率放大器ATA4014VS高压功率放大器HSA42014 一、企业背景&#xff1a; Aigtek是一家来自中国的专业从事测量仪器研发、生产和销售的高科技企业。公司主要研发和生产功率放大器、功率放大器模块、功率信号源、计量校准源等产品。核心团队主要是来自西安交通大学及西北工业大…

ERP系统介绍

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、ERP系统概述&#xff1f;1.什么是ERP2.主流ERP系统介绍3.用友ERP4.部署用友ERP畅捷通T6软件系统环境要求4.用友ERP畅捷通T6软件用户管理4.用友ERP畅捷通T6软…

简单的UDP网络程序

目录 准备工作 makefile udpServer.hpp udpServer.cc 细节1 服务端部署 创建套接字 接口认识1 socket 协议家族 绑定套接字 认识接口2 bind sockaddr_in结构体类型 细节2 bzero inet_addr 服务器启动(初启动) udpServer.hpp udpServer.cc 细节3 本地回环通…

跑通NeRF-SLAM代码记录

前言 Install 原文章github链接 下载代码 git clone https://github.com/ToniRV/NeRF-SLAM.git --recurse-submodules git submodule update --init --recursive因为有相关依赖&#xff0c;所以尽量使用命令下载代码。 2. 新建nerf-slam环境&#xff0c;github上也没提到p…

从sftp下载大文件到浏览器

从sftp下载大文件到浏览器 问题方案相关依赖包相关代码片段&#xff08;后端&#xff09;文件信息缓存工具类-FileChunkCache文件信息对象-FileDetailsftp传输进度监控-FileProgressMonitor切片工具类-ChunkService文件下载服务-AsyncDownloadService 问题 近期遇到直接使用sf…

Dubbo与SpringBoot整合

1.注意starter版本适配 2.服务提供者 创建Maven项目 boot-user-service-provider 服务提供者 2.1.通用模块依旧照用 2.2.POM <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</a…

如何在华为OD机试中获得满分?Java实现【IPv4地址转换成整数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 存在一种虚拟 IPv4<