3
×
i
3 \times i
3×i 的方格铺满骨牌的方案数,
f
[
i
]
f[i]
f[i] 的方案数不可能由
f
[
i
−
1
]
f[i-1]
f[i−1] 递推而来。那么我们猜想
f
[
i
]
f[i]
f[i] 和
f
[
i
−
2
]
f[i-2]
f[i−2] 一定是有关系的,如图二 -1-3所示,我们把第
i
i
i 列和第
i
−
1
i-1
i−1 列用
1
×
2
1 \times 2
1×2 的骨牌填满后,轻易转化成了
f
[
i
−
2
]
f[i-2]
f[i−2] 的问题,那是不是代表
f
[
i
]
=
3
∗
f
[
i
−
2
]
f[i] = 3 * f[i-2]
f[i]=3∗f[i−2] 呢?
图二 -1-3
- 仔细想想才发现不对,原因是我们少考虑了图二 -1-4的情况,这些情况用图一 -1-3的情况无法表示,再填充完黑色区域后,发现和
f
[
i
−
4
]
f[i-4]
f[i−4] 也有关系,但是还是漏掉了一些情况。
图二 -1-4
- 上面的问题说明我们在设计状态时候的思维定式,当一维的状态已经无法满足我们的需求时,我们可以试着增加一维,用二维来表示状态,用
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示
(
3
×
i
)
j
(3 \times i) + j
(3×i)+j 个多余块的摆放方案数,如图二 -1-5所示:
图二 -1-5
- 转化成二维后,我们可以轻易写出三种情况的递推式,具体推导方法见图二 -1-6。
f[0][0] = f[1][1] = f[0][2] = 1;
for(int i = 2; i <= n; ++i) {
f[i][0] = f[i-2][0] + f[i-1][1] + f[i-2][2];
f[i][1] = f[i-1][2];
f[i][2] = f[i][0] + f[i-1][1];
}
图二 -1-6
- 如果
n
n
n 不是很大的情况,到这一步,我们的问题已经完美解决了,其实并不需要求它的通项公式,因为我们是程序猿,一个 for 循环就能搞定了,接下来的求解就全仰仗于计算机来完成了。
【例题2】对一个 “01” 串进行一次 μ 变换被定义为:将其中的 “0” 变成 “10”,“1” 变成 “01”,初始串为 “1”,求经过
n
(
n
≤
1000
)
n(n \le 1000)
n(n≤1000) 次 μ 变换后的串中有多少对 “00”(有没有人会纠结会不会出现 “000” 的情况?这个请放心,由于问题的特殊性,不会出现 “000” 的情况)。图二 -1-7表示经过小于4次变换时串的情况。
图二 -1-7
- 如果纯模拟的话,每次 μ 变换串的长度都会加倍,所以时间和空间复杂度都是
O
(
2
n
)
O(2^n)
O(2n),对于
n
=
1000
n = 1000
n=1000 的情况,完全不可能计算出来。仔细观察这个树形结构,可以发现要出现 00,一定是 10 和 01 相邻产生的。为了将问题简化,我们不妨设:
A
=
10
,
B
=
01
A = 10, B = 01
A=10,B=01
- 构造出的树形递推图如图二 -1-8所示,如果要出现 00,一定是 AB(1001)。
图二 -1-8
- 令
f
[
i
]
[
0
]
f[i][0]
f[i][0] 为 A 经过 i 次 μ 变换后 00 的数量,则
f
[
0
]
[
0
]
=
0
f[0][0] = 0
f[0][0]=0,
f
[
i
]
[
1
]
f[i][1]
f[i][1] 为 B 经过
i
i
i 次 μ 变换后 00 的数量,
f
[
0
]
[
1
]
=
0
f[0][1] = 0
f[0][1]=0。
- 从图中观察得出,以A为根的树,它的左子树的最右端点一定是B,也就是说无论经过多少次变换,两棵子树的交界处都不可能产生AB,所以有:
- f
[
i
]
[
0
]
=
f
[
i
−
1
]
[
0
]
f
[
i
−
1
]
[
1
]
f[i][0] = f[i-1][0] + f[i-1][1]
f[i][0]=f[i−1][0]+f[i−1][1]
- 而以 B 为根的树,它的左子树的右端点一定是A,而右子树的左端点呈BABABA…交替排布,所以隔代产生一次AB,于是
f
[
i
]
[
1
]
=
f
[
i
−
1
]
[
0
]
f
[
i
−
1
]
[
1
]
(
i
m
o
d
2
)
f[i][1] = f[i-1][0] + f[i-1][1] + (i \ mod \ 2)
f[i][1]=f[i−1][0]+f[i−1][1]+(i mod 2) 最后要求的答案就是
f
[
n
−
1
]
[
1
]
f[n-1][1]
f[n−1][1],递推求解。
f[0][0] = f[0][1] = 0;
for(int i = 1; i <= 1000; i++) {
f[i][0] = f[i-1][0] + f[i-1][1];
f[i][1] = f[i-1][0] + f[i-1][1] + (i % 2);
}
2、状态和状态转移
- 在介绍递推的时候,涉及到一个词—状态,它表示了解决某一问题的中间结果,这是一个比较抽象的概念,例如【例题1】中的
f
[
i
]
[
j
]
f[i][j]
f[i][j],【例题2】中的
f
[
i
]
[
0
]
f[i][0]
f[i][0]、
f
[
i
]
[
1
]
f[i][1]
f[i][1],求解问题的时候,首先要设计出合适的状态,然后通过状态的特征建立状态转移方程(
f
[
i
]
=
f
[
i
−
1
]
f
[
i
−
2
]
f[i] = f[i-1] + f[i-2]
f[i]=f[i−1]+f[i−2] 就是一个简单的状态转移方程)。
- 下文第四节会图解常用的状态转移方程。
3、最优化原理和最优子结构
- 如果问题的最优解包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。这里我尽力减少理论化的概念,而改用一个简单的例题来加深对这句话的理解。
【例题3】给定一个长度为
n
(
1
<
=
n
<
=
1000
)
n(1 <= n <= 1000)
n(1<=n<=1000) 的整数序列
a
[
i
]
a[i]
a[i],求它的一个子序列 (子序列即在原序列任意位置删除0或多个元素后的序列),满足如下条件:
1、该序列单调递增;
2、在所有满足条件 1 的序列中长度是最长的;
- 这个问题是经典的动态规划问题,被称为最长单调子序列。
- 我们假设现在没有任何动态规划的基础,那么看到这个问题首先想到的是什么?
- 我想到的是万金油算法—深度优先搜索(
D
F
S
DFS
DFS ),即枚举
a
[
i
]
a[i]
a[i] 这个元素取或不取,所有取的元素组成一个合法的子序列,枚举的时候需要满足单调递增这个限制,那么对于一个
n
n
n 个元素的序列,最坏时间复杂度自然就是
O
(
2
n
)
O(2^n)
O(2n) ,
n
=
30
n = 30
n=30 就已经很变态了更别说是
1000
1000
1000。
- 然而,方向是对的,动态规划求解之前先试想一下搜索的正确性,这里搜索的正确性是很显然的,因为已经枚举了所有情况,总有一种情况是我们要求的解。我们尝试将搜索的算法进行一些改进,假设第
i
i
i 个数取的情况下已经搜索出的最大长度记录在数组中,即用
d
[
i
]
d[i]
d[i] 表示当前搜索到的以
a
[
i
]
a[i]
a[i] 结尾的最长单调子序列的长度,那么如果下次搜索得到的序列长度小于等于
d
[
i
]
d[i]
d[i],就不必往下搜索了(因为即便继续往后枚举,能够得到的解必定不会比之前更长);反之,则需要更新
d
[
i
]
d[i]
d[i] 的值。
- 如图二-3-1,红色路径表示第一次搜索得到的一个最长子序列1、2、3、5,蓝色路径表示第二次搜索,当枚举第3个元素取的情况时,发现以第3个数结尾的最长长度
d
[
3
]
=
3
d[3] = 3
d[3]=3,比本次枚举的长度要大(本次枚举的长度为2),所以放弃往下枚举,大大减少了搜索的状态空间。
图二-3-1
- 这时候,我们其实已经不经意间设计好了状态,就是上文中提到的那个
d
[
i
]
d[i]
d[i] 数组,它表示的是以
a
[
i
]
a[i]
a[i] 结尾的最长单调子序列的长度,那么对于任意的
i
i
i,
d
[
i
]
d[i]
d[i] 一定等于
d
[
j
]
1
(
j
<
i
)
d[j] + 1 \ ( j < i )
d[j]+1 (j<i),而且还得满足
a
[
j
]
<
a
[
i
]
a[j] < a[i]
a[j]<a[i]。因为这里的
d
[
i
]
d[i]
d[i] 表示的是最长长度,所以
d
[
i
]
d[i]
d[i] 的表达式可以更加明确,即:
d
[
i
]
=
m
a
x
(
d
[
j
]
∣
j
<
i
,
a
[
j
]
<
a
[
i
]
)
1
d[i] = max ( d[j] | j < i, a[j] < a[i] ) + 1
d[i]=max(d[j]∣j<i,a[j]<a[i])+1
- 这个表达式很好的阐释了最优化原理,其中
d
[
j
]
d[j]
d[j] 作为
d
[
i
]
d[i]
d[i] 的子问题,
d
[
i
]
d[i]
d[i] 最长(优)当且仅当
d
[
j
]
d[j]
d[j] 最长(优)。当然,这个方程就是这个问题的状态转移方程。状态总数量
O
(
n
)
O(n)
O(n), 每次转移需要用到前
i
i
i 项的结果,平摊下来也是
O
(
n
)
O(n)
O(n) 的, 所以该问题的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。
4、决策和无后效性
- 一个状态演变到另一个状态,往往是通过“决策”来进行的。有了“决策”,就会有状态转移。而无后效性,就是一旦某个状态确定后,它之前的状态无法对它之后的状态产生“效应”(影响)。
【例题4】老王想在未来的
n
n
n 年内每年都持有电脑,
m
(
y
,
z
)
m(y, z)
m(y,z) 表示第
y
y
y 年到第
z
z
z 年的电脑维护费用,其中
y
y
y 的范围为
[
1
,
n
]
[1, n]
[1,n],
z
z
z 的范围为
[
y
,
n
]
[y, n]
[y,n],
c
c
c 表示买一台新的电脑的固定费用。 给定矩阵
m
m
m,固定费用
c
c
c,求在未来
n
n
n 年都有电脑的最少花费。
- 考虑第
i
i
i 年是否要换电脑,换和不换是不一样的决策,那么我们定义一个二元组
(
a
,
b
)
(a, b)
(a,b),其中
a
<
b
a < b
a<b,它表示了第 a 年和第 b 年都要换电脑(第 a 年和第 b 年之间不再换电脑),如果假设我们到第 a 年为止换电脑的最优方案已经确定,那么第 a 年以前如何换电脑的一些列步骤变得不再重要,因为它并不会影响第 b 年的情况,这就是无后效性。
- 接下来,会对这题进行一个详细的解释,当然看不懂没关系,可以跳过这个步骤,直接去看 第三章 - 动态规划的经典模型。毕竟,本文是入门级别的,后面还会花更多的时间来讲解动态规划的内容,可以和搜索一起逐步理解状态的概念。
- 更加具体得,令
d
[
i
]
d[i]
d[i] 表示在第 i 年买了一台电脑的最小花费(由于这台电脑能用多久不确定,所以第 i 年的维护费用暂时不计在这里面),如果上一次更换电脑的时间在第 j 年,那么第 j 年更换电脑到第 i 年之前的总开销就是
c
m
(
j
,
i
−
1
)
c + m(j, i-1)
c+m(j,i−1)
- 于是有状态转移方程:
d
[
i
]
=
m
i
n
(
d
[
j
]
m
(
j
,
i
−
1
)
∣
1
<
=
j
<
i
)
c
d[i] = min( d[j] + m(j, i-1) | 1 <= j < i ) + c
d[i]=min(d[j]+m(j,i−1)∣1<=j<i)+c
- 这里的
d
[
i
]
d[i]
d[i] 并不是最后问题的解,因为它漏算了第 i 年到第 n 年的维护费用,所以最后问题的答案:
a
n
s
=
m
i
n
(
d
[
i
]
m
(
i
,
n
)
∣
1
<
=
i
<
n
)
ans = min( d[i] + m(i, n) | 1 <= i < n )
ans=min(d[i]+m(i,n)∣1<=i<n)
- 我们发现两个方程看起来很类似,其实是可以合并的,我们可以假设第 n+1 年必须换电脑,并且第 n+1 年换电脑的费用为 0,那么整个阶段的状态转移方程就是:
d
[
i
]
=
m
i
n
(
d
[
j
]
m
(
j
,
i
−
1
)
∣
1
<
=
j
<
i
)
w
(
i
)
d[i] = min( d[j] + m(j, i-1) | 1 <= j < i ) + w(i)
d[i]=min(d[j]+m(j,i−1)∣1<=j<i)+w(i)
w
(
i
)
=
{
c
i
<
n
1
0
i
=
n
1
w(i) = \begin{cases} c & i < n+1\ 0 & i=n+1 \end{cases}
w(i)={c0i<n+1i=n+1
- d
[
n
1
]
d[n+1]
d[n+1] 就是我们需要求的最小费用了。
三、动态规划的经典模型
- 本章节作者会通过图的方式,带读者了解一些基本模型,以加深对动态规划状态的理解;
- 黄色 ■ 代表当前状态;
- 绿色 ■ 代表子状态(已经求出的状态);
- 红色 ■ 代表尚未求出的状态;
- 灰色 ■ 代表永远不存在的状态;
1、线性
作者徽是vip1024c
模型
- 线性模型是动态规划中最常见的模型,上文讲到的最长单调子序列就是经典的线性模型。
- 线性模型的状态一般是通过 一维数组表示的,如图三-1-1所示,图中黄色块的状态为
d
[
i
]
d[i]
d[i],绿色块的状态为
d
[
j
]
d[j]
d[j],并且满足
(
j
<
i
)
(j < i)
(j<i),只有当
d
[
j
]
d[j]
d[j] 全部计算出来以后,
d
[
i
]
d[i]
d[i]的值才能够被确定。
图三-1-1
- 线性模型最经典的问题莫过于 背包问题 了,有关背包问题的内容,可以参考以下这篇文章:夜深人静写算法(十九)- 背包总览。
2、区间模型
- 对比线性模型,区间模型状态一般是通过:一个二维数组来表示的。
- 区间模型的状态表示一般为
d
[
i
]
[
j
]
d[i][j]
d[i][j],表示区间
[
i
,
j
]
[i, j]
[i,j] 上的最优解,最终要求的肯定是
[
1
,
n
]
[1, n]
[1,n] 的最优解。
- 如图三-2-1所示,既然是表示区间,所以对于状态
d
[
i
]
[
j
]
d[i][j]
d[i][j],当
i
j
i>j
i>j时,肯定是不合法的状态,所以标记为灰色;
d
[
i
]
[
j
]
d[i][j]
d[i][j] 表当前状态,标记为黄色;
图三-2-1
- 区间模型的详细内容可以参考以下这篇文章:夜深人静写算法(二十七)- 区间DP。
3、树状模型
- 树形动态规划(树形DP),是指状态图是一棵树,状态转移也发生在树上,父结点的状态值通过所有子结点状态值计算完毕后得出,后续会专门开辟一个章节来讲述树形动态规划。
- 状态表示如图三-3-1所示。
图三-3-1
4、状态压缩模型
- 状态压缩的含义其实是对状态进行重新编码,来看下面这个例子。
- 假设状态是一个五维的数组,并且每一维的取值为
[
0
,
3
]
[0,3]
[0,3],状态表示如下:
d
[
a
]
[
b
]
[
c
]
[
d
]
[
e
]
(
0
<
=
a
,
b
,
c
,
d
,
e
<
=
3
)
d[a][b][c][d][e] \ (0 <= a,b,c,d,e <= 3)
d[a][b][c][d]e
- 那么,写代码的过程中需要操作五维数组,十分繁琐,我们可以通过将状态压缩,将它重新编码到一个一维数组中。
- 其实只要能够找到一个映射函数,满足
x
x
x 和
(
a
,
b
本人从事网路安全工作12年,曾在2个大厂工作过,安全服务、售后服务、售前、攻防比赛、安全讲师、销售经理等职位都做过,对这个行业了解比较全面。
最近遍览了各种网络安全类的文章,内容参差不齐,其中不伐有大佬倾力教学,也有各种不良机构浑水摸鱼,在收到几条私信,发现大家对一套完整的系统的网络安全从学习路线到学习资料,甚至是工具有着不小的需求。
最后,我将这部分内容融会贯通成了一套282G的网络安全资料包,所有类目条理清晰,知识点层层递进,需要的小伙伴可以点击下方小卡片领取哦!下面就开始进入正题,如何从一个萌新一步一步进入网络安全行业。
学习路线图
其中最为瞩目也是最为基础的就是网络安全学习路线图,这里我给大家分享一份打磨了3个月,已经更新到4.0版本的网络安全学习路线图。
相比起繁琐的文字,还是生动的视频教程更加适合零基础的同学们学习,这里也是整理了一份与上述学习路线一一对应的网络安全视频教程。
网络安全工具箱
当然,当你入门之后,仅仅是视频教程已经不能满足你的需求了,你肯定需要学习各种工具的使用以及大量的实战项目,这里也分享一份我自己整理的网络安全入门工具以及使用教程和实战。
项目实战
最后就是项目实战,这里带来的是SRC资料&HW资料,毕竟实战是检验真理的唯一标准嘛~
面试题
归根结底,我们的最终目的都是为了就业,所以这份结合了多位朋友的亲身经验打磨的面试题合集你绝对不能错过!