数据结构与算法学习笔记14:动态规划 / 位运算 / 格雷码 / 异或
动态规划(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(1−1)+1=1={f(2−2)+1=1f(2−1)+1=2={f(3−2)+1=2f(3−1)+1=2={f(4−2)+1=2f(4−1)+1=3=⎩⎪⎨⎪⎧f(5−5)+1=1f(5−2)+1=3f(5−1)+1=3=min[f(i−v[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 0到10为数组下标索引。
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][j−1]和
C
[
i
−
1
]
[
j
]
C[i-1][j]
C[i−1][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[i−1][j]−c[i][j−1]]+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[i−1][j−1],
c
[
i
−
1
]
[
j
]
c[i-1][j]
c[i−1][j]与
c
[
i
]
[
j
−
1
]
c[i][j-1]
c[i][j−1]均已计算出来。此时我们根据
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[i−1][j−1]+1ifi,j>0andxi=yimax(c[i][j−1],c[i−1][j])ifi,j>0andxi=yj
- 实际上,如果遇到题目求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} 给一个整数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
- 实际就是格雷码的运用,将该数转换为二进制1000,将1000看作格雷码,然后转换为二进制1111,和0相减得到15便是操作次数。