数据结构与算法学习笔记14:动态规划 / 位运算 / 格雷码 / 异或

本文探讨了动态规划在凑钱问题、最长递增子序列、捡苹果问题和最长公共子序列等经典问题中的应用,强调了无后效性和最优子结构的重要性。同时,介绍了位运算在寻找丢失元素和优化加法操作中的巧妙运用,并讲解了格雷码的转换方法。通过实例解析,展示了如何将动态规划和位运算结合以提高算法效率。
摘要由CSDN通过智能技术生成

动态规划(Dynamic Programming / DP)

将一个大问题拆解成几个子问题,分别求解这些子问题,即可推断出大问题的解。动态规划和分治法很类似,但动态规划并 不满足分治法中“子问题的解相互独立” 这一性质。空间换时间。有点暴力的剪枝的感觉,暴力是把所有的都列出来,动态规划是只要可行解。

  • 无后效性: 如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。(未来与过去无关,一旦f(w)确定,就不关心如何凑出f(w)了)

  • 最优子结构:大问题的最优解可以由小问题的最优解推出。

  • 一个问题能否用动态规划来解决?

    • ① 大问题可以拆解成几个子问题;② 满足无后效性;③ 满足最优子结构性质。
通用步骤:

1、分阶段(大问题->子问题)

2、找状态(即子问题的最优解)

3、状态转移方程

4、寻找终止条件

凑钱问题

有1、2、5面额,凑n元要几张?
f ( 0 ) = 0 f ( 1 ) = f ( 1 − 1 ) + 1 = 1 f ( 2 ) m i n = { f ( 2 − 2 ) + 1 = 1 f ( 2 − 1 ) + 1 = 2 f ( 3 ) m i n = { f ( 3 − 2 ) + 1 = 2 f ( 3 − 1 ) + 1 = 2 f ( 4 ) m i n = { f ( 4 − 2 ) + 1 = 2 f ( 4 − 1 ) + 1 = 3 f ( 5 ) m i n = { f ( 5 − 5 ) + 1 = 1 f ( 5 − 2 ) + 1 = 3 f ( 5 − 1 ) + 1 = 3 f ( i ) = m i n [ f ( i − v [ j ] ) ] + 1 \begin{aligned} f(0)&=0\\ f(1)&=f(1-1)+1=1\\ f(2)_{min}&=\begin{cases} f(2-2)+1=1\\ f(2-1)+1=2 \end{cases}\\ f(3)_{min}&=\begin{cases} f(3-2)+1=2\\ f(3-1)+1=2 \end{cases}\\ f(4)_{min}&=\begin{cases} f(4-2)+1=2\\ f(4-1)+1=3 \end{cases}\\ f(5)_{min}&=\begin{cases} f(5-5)+1=1\\ f(5-2)+1=3\\ f(5-1)+1=3 \end{cases}\\ f(i)&=min[f(i-v[j])]+1 \end{aligned} f(0)f(1)f(2)minf(3)minf(4)minf(5)minf(i)=0=f(11)+1=1={f(22)+1=1f(21)+1=2={f(32)+1=2f(31)+1=2={f(42)+1=2f(41)+1=3=f(55)+1=1f(52)+1=3f(51)+1=3=min[f(iv[j])]+1

最长递增子序列 LIS

子序列:可以不连续但相对位置不变 // 子数组:相对位置不变且连续

题目序列:3 9 1 4 12 7 6 8 5 2

在这里插入图片描述

  • 优化:

    本题所求的是最长递增子序列的长度而非每个序列的实际情况,观察上述解法,有些步骤是多余的,比如 f ( 4 ) m a x f(4)_{max} f(4)max中的 12 > 1 , 12 > 9 , 12 > 3 12>1,12>9,12>3 12>1,12>9,12>3,可以通过获取相同长度的递增子序列的末尾数字进行比较,取末尾最小的进行计算即可达到简化。

    题目序列:3 9 1 4 12 7 6 8 5 2

    建立 W [ j ] W[j] W[j]数组进行递增子序列的末尾数字的存放,首行 0 到 10 0到10 010为数组下标索引。
    0 1 2 3 4 5 6 7 8 9 10 / \begin{array}{|c|c|c|c|c|c|c|c|c|c|c|} \hline 0&1&2&3&4&5&6&7&8&9&10\\ \hline /&&&&&&&&&&\\ \hline \end{array} 0/12345678910
    在这里插入图片描述

  • 继续优化:可以看出 w [ j ] w[j] w[j]为有序数列,而每次进行新元素的子序列分析时,目前是从 w [ j ] w[j] w[j]中存有数据的最后一位开始的,但实际上,比如 2 < 8 , 2 < 5 , 2 < 4 2<8,2<5,2<4 2<8,2<5,2<4这些比较是繁琐的,可以使用二分法进行优化~

捡苹果问题

平面上有M*N个格子,每个格子中放着一定数量的苹果。从左上角的格子开始, 每一步只能向下走或是向右走,每次走到一个格子就把格子里的苹果收集起来, 这样一直走到右下角,问最多能收集到多少个苹果。

A [ m ] [ n ] A[m][n] A[m][n]表示该格子存放的苹果数量, C [ i ] [ j ] C[i][j] C[i][j]为状态,即走到该格子所收集到的苹果数量,那么能够到达 C [ i ] [ j ] C[i][j] C[i][j]处的只有两个位置 C [ i ] [ j − 1 ] C[i][j-1] C[i][j1] C [ i − 1 ] [ j ] C[i-1][j] C[i1][j],所以必然是取这两个位置中比较大的那一个点。
c [ i ] [ j ] = m a x [ c [ i − 1 ] [ j ] − c [ i ] [ j − 1 ] ] + A [ i ] [ j ] c[i][j]=max[c[i-1][j]-c[i][j-1]]+A[i][j] c[i][j]=max[c[i1][j]c[i][j1]]+A[i][j]
在这里插入图片描述

最长公共子序列 LCS

引进一个二维数组 c [ ] [ ] c[][] c[][],用 c [ i ] [ j ] c[i][j] c[i][j]记录 X [ i ] X[i] X[i] Y [ j ] Y[j] Y[j] 的LCS 的长度,在计算 c [ i ] [ j ] c[i][j] c[i][j]之前, c [ i − 1 ] [ j − 1 ] c[i-1][j-1] c[i1][j1] c [ i − 1 ] [ j ] c[i-1][j] c[i1][j] c [ i ] [ j − 1 ] c[i][j-1] c[i][j1]均已计算出来。此时我们根据 X [ i ] = Y [ j ] X[i] = Y[j] X[i]=Y[j]还是 X [ i ] ! = Y [ j ] X[i] != Y[j] X[i]!=Y[j],就可以计算出 c [ i ] [ j ] c[i][j] c[i][j]
c [ i ] [ j ] = { 0 ,                                                                                            i f        i = 0        o r        j = 0 c [ i − 1 ] [ j − 1 ] + 1                                            i f        i , j > 0      a n d      x i = y i m a x ( c [ i ] [ j − 1 ] , c [ i − 1 ] [ j ] )                i f        i , j > 0      a n d      x i ≠ y j \begin{aligned} c[i][j]=&\begin{cases} 0,\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;if\;\;\;i = 0\;\;\;or\;\;\;j = 0\\ c[i-1][j-1]+1\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;if\;\;\;i,j>0\;\;and\;\;x_i=y_i\\ max(c[i][j-1],c[i-1][j])\;\;\;\;\;\;\;if\;\;\;i,j>0\;\;and\;\;x_i≠y_j \end{cases}\\ \end{aligned} c[i][j]=0,ifi=0orj=0c[i1][j1]+1ifi,j>0andxi=yimax(c[i][j1],c[i1][j])ifi,j>0andxi=yj
img

  • 实际上,如果遇到题目求LIS(最长递增子序列),但实在不记得LIS的解法,也可采用LCS的方式求解(将原序列进行排序,然后求原序列和经排序后的LCS即可得到LIS)

区间调度

多个[start,end]闭区间,最多有几个互不相交?(边界相交不算相交)

  • 1、 将所有区间按照end从小到大排序;
  • 2、找到最小的end;
  • 3、遍历集合找start(若start<end相交则不符合,start≥end不相交则合并);

“^” 异或

相消性找元素特殊情况

n个元素1到n之间,无重复,有一个元素丢失,请快速找到丢失的元素

  • 方案:

    1、由于1到n无重复,所以分别在丢失前后求Sum然后进行比较相减即可得到。时间消耗 O ( n ) O(n) O(n)但需要注意,求和连续相加很有可能导致溢出

    2、排序+下标二分检索,看哪个位置的元素缺失,但排序的时间消耗哪怕是最快的也比较大 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),而且排序完还要搜索。

    3、利用异或 “^” ,对丢失前数组进行异或操作得到 R 1 R_1 R1,对丢失后数组进行异或操作得到 R 2 R_2 R2,然后再对这两个结果进行异或操作可以消掉相同的,剩下那个就是丢失的。没溢出问题,且时间空间消耗都很低。

1+n个元素,在1到n之间,有一个重复为两次,其他均不重复为一次,请快速找到那个重复的元素。

  • 利用异或 “^” ,不重复( ”A ^ A“ 消除),重复( ”B ^ B ^ B“ )消除后又回到 B。
  • 利用异或的消除性,奇数和偶数个遗留情况不同,因而可以辨别出来。
A^=B和 A=A^B 的底层系统效率
  • A^=B效率高

PS:不能对同一块空间使用^

  • 同一空间同一内容,使用异或后内容就消失了。

  • 想要交换时无须判断是否属于同一空间,相同内容不进行异或即可,两种写法如下:

在这里插入图片描述

后者比前者效率优化,更高,原因在于AB缓存可以在后面计算A=B时被使用,节省计算时间。

系统底层的^
  • if(x!=y) 底层实现为 (x^y)!=0 而非 x-y!=0
  • cpu 清零时实际上用的都是异或自身 A^A消除后得到0

不用加减乘除实现加法

a = 5 ; b = 13 , 不 使 用 加 减 乘 除 实 现 加 法 。 a=5;b=13,不使用加减乘除实现加法。 a=5;b=13,使
5在二进制中表示为 0101,13在二进制中表示为1101(确定二进制的方式是从距离该数最近的二的次幂开始找),5+13的和为18,其用二进制可以表示为10010,我们将三个二进制数列竖式如下:
在这里插入图片描述

可以发现:11相加为0进位1,00相加仍然为0,01相加则为1,因此可以将进位和无进位的利用按位与操作及左移和异或表示出来,再完成左移结果和异或结果的相加,重复操作直到需要进位的全为0,则该加法得到最终解,过程如下:

  • 代码实现:

    //bitwise operation 位运算
    //不使用加减乘除实现加法运算
    
    #include <stdio.h>
    
    int Add(int a,int b){
        int Xor;
        int And;
        while(1){
            //进位
            And = a & b;
            And <<= 1;
            //不需要进位的
            Xor = a ^ b;
    		
            if (And == 0) return Xor;
            a = And;
            b = Xor;
        }
    }
    
    int main(){
        printf("%d",Add(311, 423));
        return 0;
    }
    

格雷码(Gray Code)

在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码。

二进制码如何转换为格雷码?

保留二进制码的最高位作为格雷码的最高位,次高位格雷码为二进制码的高位与次高位相异或,其余各位与次高位的求法一样。

代码:B^(B>>1)

    //根据二进制转换成格雷码的法则,可以得到以下的代码:
      static unsigned int DecimaltoGray(unsigned int x)
      {
         return x^(x>>1);
      }//以上代码实现了unsigned int型数据到格雷码的转换,最高可转换32位自然二进制码,超出32位将溢出。 
      static  int DecimaltoGray( int x)
      {
         return x^(x>>1);
      }//以上代码实现了 int型数据到格雷码的转换,最高可转换31位自然二进制码,超出31位将溢出。       
格雷码如何转换为二进制码?

保留格雷码的最高位作为自然二进制码的最高位,而次高位自然二进制码为高位自然二进制码与次高位格雷码相异或,其余各位与次高位自然二进制码的求法相类似。

//根据二进制格雷码转换成自然二进制码的法则,可以得到以下的三种代码方式: 
       static unsigned int GraytoDecimal(unsigned int x)
       {
          unsigned int y = x;
          while(x>>=1)
            y ^= x;
          return y;
       } 
       static unsigned int GraytoDecimal(unsigned int x)
       {
          x^=x>>16;
          x^=x>>8;
          x^=x>>4;
          x^=X>>2;
          x^=x^1;
          return x;
       }       
       static unsigned int GraytoDecimal(unsigned int x)
       {
          int i;
          for(i=0;(1<<i)<sizeof(x)*8;i++)
          {
             x^=x>>(1<<i);
          }
          return x;
       }        
//以上代码实现了unsigned int型数据到自然二进制码的转换,最高可转换32位格雷码,超出32位将溢出。
//类型改为int型即可实现31位格雷码转换。
翻转整数(实际上就是二进制转换为格雷码的过程)

给 一 个 整 数 n , 需 要 重 复 多 少 次 如 下 操 作 , 才 能 够 将 数 字 转 化 为 0 ? ① 翻 转 当 前 数 字 对 应 的 二 进 制 位 里 最 右 侧 的 一 位 ; ② 如 果 二 进 制 里 的 第 i − 1 位 为 1 , 第 i − 2 位 至 第 0 位 均 为 0 , 可 以 翻 转 第 i 位 。 ( 二 进 制 中 只 有 0    1 , 翻 转 意 思 就 是 把 0 变 1 , 把 1 变 0 ) 例 如 : 整 数 8 , 1000 → 1001 → 1011 → 1010 → 1110 → 1111 → 1101 → 1100 → 0100                            → 0101 → 0111 → 0110 → 0010 → 0011 → 0001 → 0000 \begin{aligned} &给一个整数n,需要重复多少次如下操作,才能够将数字转化为0?\\ &①翻转当前数字对应的二进制位里最右侧的一位;\\ &②如果二进制里的第i-1位为1,第i-2位至第0位均为0,可以翻转第i位。\\ &(二进制中只有0\ \ 1,翻转意思就是把0变1,把1变0)\\ & 例如:整数8,1000→1001→1011→1010→1110→1111→1101→1100→0100\\ & \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \;\;\;\;\;→0101→0111→0110→0010→0011→0001→0000 \end{aligned} n0i11i200i0  101108100010011011101011101111110111000100               0101011101100010001100010000

  • 实际就是格雷码的运用,将该数转换为二进制1000,将1000看作格雷码,然后转换为二进制1111,和0相减得到15便是操作次数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

97Marcus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值