【深度强化学习】4. Policy Gradient

news/2024/5/19 1:53:03 标签: 人工智能, 强化学习, 深度学习, 神经网络

【Datawhale打卡】十一的时候自己看过一遍,李宏毅老师讲的很好,对数学小白也很友好,但是由于没有做笔记(敲代码),看完以后脑袋里空落落的。趁着这次打卡活动,重新看一遍,果然好多细节需要重头梳理一遍。

文章目录

    • 1. 新概念/符号
    • 2. 三个组成部分
    • 3. Gradient Ascent
    • 4. 实现/实做
      • 4.1 TIP1 Add a Baseline
      • 4.2 TIP2 Assign Suitable Credit
    • 5. MC & TD
      • 5.1 MC-REINFORCE
      • 5.2 TD-Actor Critic
    • 6. 问答
    • 7. 参考

1. 新概念/符号

  • policy(策略): 每一个actor中会有对应的策略,这个策略决定了actor的行为。(给定一个state,policy会决定action)。policy记为 π \pi π
  • Return(回报): 一个回合(Episode)所得到的所有的reward的总和,也称为Total reward。一般地,用 R R R 来表示。
  • Trajectory(轨迹 τ \tau τ ): 一个试验中将environment 输出的 s s s 跟 actor 输出的行为 a a a,把这个 s s s a a a 全部串起来形成的集合,称为Trajectory,即  Trajectory  τ = { s 1 , a 1 , s 2 , a 2 , ⋯   , s t , a t } \text { Trajectory } \tau=\left\{s_{1}, a_{1}, s_{2}, a_{2}, \cdots, s_{t}, a_{t}\right\}  Trajectory τ={s1,a1,s2,a2,,st,at}
  • Reward function: 根据在某一个 state 采取的某一个 action 决定说现在这个行为可以得到多少的分数,它是一个 function。也就是给一个 s 1 s_1 s1 a 1 a_1 a1,它告诉你得到 r 1 r_1 r1。给它 s 2 s_2 s2 a 2 a_2 a2,它告诉你得到 r 2 r_2 r2。 把所有的 r r r 都加起来,就得到了 R ( τ ) R(\tau) R(τ) ,代表某一个 trajectory τ \tau τ 的 reward。
  • Expected reward: R ˉ θ = ∑ τ R ( τ ) p θ ( τ ) = E τ ∼ p θ ( τ ) [ R ( τ ) ] \bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau)=E_{\tau \sim p_{\theta}(\tau)}[R(\tau)] Rˉθ=τR(τ)pθ(τ)=Eτpθ(τ)[R(τ)]
符号解释
τ \tau τ轨迹,游戏从开始到结束的s、a串( { s 1 , a 1 , s 2 , a 2 , ⋯   , s t , a t } \left\{s_{1}, a_{1}, s_{2}, a_{2}, \cdots, s_{t}, a_{t}\right\} {s1,a1,s2,a2,,st,at}
episode一个游戏回合,从开始到结束
π \pi πPolicy 策略的代指符号
θ \theta θPolicy π \pi π中的参数

2. 三个组成部分

示意图

强化学习通常有以下组成部分,actor, environment, reward。具体过程如上图所示,构成了一个完整的轨迹Trajectory:
 Trajectory  τ = { s 1 , a 1 , s 2 , a 2 , ⋯   , s t , a t } \text { Trajectory } \tau=\left\{s_{1}, a_{1}, s_{2}, a_{2}, \cdots, s_{t}, a_{t}\right\}  Trajectory τ={s1,a1,s2,a2,,st,at}
每一个 trajectory,你可以计算它发生的概率。假设现在 actor 的参数已经被给定了话,就是 θ \theta θ。根据 θ \theta θ,你其实可以计算某一个 trajectory 发生的概率,你可以计算某一个回合,某一个 episode 里面, 发生这样子状况的概率。
p θ ( τ ) = p ( s 1 ) p θ ( a 1 ∣ s 1 ) p ( s 2 ∣ s 1 , a 1 ) p θ ( a 2 ∣ s 2 ) p ( s 3 ∣ s 2 , a 2 ) ⋯ = p ( s 1 ) ∏ t = 1 T p θ ( a t ∣ s t ) p ( s t + 1 ∣ s t , a t ) \begin{aligned} p_{\theta}(\tau) &=p\left(s_{1}\right) p_{\theta}\left(a_{1} | s_{1}\right) p\left(s_{2} | s_{1}, a_{1}\right) p_{\theta}\left(a_{2} | s_{2}\right) p\left(s_{3} | s_{2}, a_{2}\right) \cdots \\ &=p\left(s_{1}\right) \prod_{t=1}^{T} p_{\theta}\left(a_{t} | s_{t}\right) p\left(s_{t+1} | s_{t}, a_{t}\right) \end{aligned} pθ(τ)=p(s1)pθ(a1s1)p(s2s1,a1)pθ(a2s2)p(s3s2,a2)=p(s1)t=1Tpθ(atst)p(st+1st,at)

注:以上的P函数分为两种!

1 p ( s t + 1 ∣ s t , a t ) p\left(s_{t+1} | s_{t}, a_{t}\right) p(st+1st,at)函数代表环境,当前环境,如果在状态st下使用at,转化到 s t + 1 s_{t+1} st+1的概率。

2 p θ ( a t ∣ s t ) p_{\theta}(a_t|s_t) pθ(atst)函数代表actor/agent的policy,如果在状态st下使用at的概率。

Reward Function: 根据在某一个 state 采取的某一个 action 决定说现在这个行为可以得到多少的分数。

给它 s 1 s_1 s1 a 1 a_1 a1,它告诉你得到 r 1 r_1 r1。给它 s 2 s_2 s2 a 2 a_2 a2,它告诉你得到 r 2 r_2 r2。 把所有的 r r r 都加起来,就得到了 R ( τ ) R(\tau) R(τ) , 目标就是通过调整actor的参数 θ \theta θ, 让R的值越大越好。由于actor采取action的时候存在随机性,所以R也是一个随机变量,采用以下公式估计其期望值:
R ˉ θ = ∑ τ R ( τ ) p θ ( τ ) = E τ ∼ p θ ( τ ) [ R ( τ ) ] \bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau)=E_{\tau \sim p_{\theta}(\tau)}[R(\tau)] Rˉθ=τR(τ)pθ(τ)=Eτpθ(τ)[R(τ)]
这个计算方式也就是:穷举所有可能的 trajectory τ \tau τ, 每一个 trajectory τ \tau τ 都有一个概率。同时也可以表达为期望的形式,从 p θ ( τ ) p_{\theta}(\tau) pθ(τ) 这个 distribution sample 一个 trajectory τ \tau τ,然后计算 R ( τ ) R(\tau) R(τ) 的期望值,就是你的 expected reward。 目标就是 maximize expected reward。

3. Gradient Ascent

对以下式子希望得到maximize expected reward, 要使用Gradient Ascent算法,先计算R的梯度gradient。
R ˉ θ = ∑ τ R ( τ ) p θ ( τ ) \bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau) Rˉθ=τR(τ)pθ(τ)
由于只有 p θ ( τ ) p_{\theta}(\tau) pθ(τ)是和参数 θ \theta θ相关的,所以梯度只需要对这部分求即可。

存在以下公式:

由:
d ( l n x ) d x = 1 x \frac{d(lnx)}{dx}=\frac{1}{x} dxd(lnx)=x1
得:
∇ f ( x ) = f ( x ) ∇ l o g f ( x ) \nabla f(x)=f(x)\nabla logf(x) f(x)=f(x)logf(x)
所以带入 p θ p_\theta pθ以后有:
∇ p θ ( τ ) p θ ( τ ) = ∇ l o g p θ ( τ ) \frac{\nabla p_\theta(\tau)}{p_\theta(\tau)}=\nabla log p_\theta(\tau) pθ(τ)pθ(τ)=logpθ(τ)
推导过程如下:
∇ R ˉ θ = ∑ τ R ( τ ) ∇ p θ ( τ ) = ∑ τ R ( τ ) p θ ( τ ) ∇ p θ ( τ ) p θ ( τ ) = ∑ τ R ( τ ) p θ ( τ ) ∇ log ⁡ p θ ( τ ) = E τ ∼ p θ ( τ ) [ R ( τ ) ∇ log ⁡ p θ ( τ ) ] \begin{aligned} \nabla \bar{R}_{\theta}&=\sum_{\tau} R(\tau) \nabla p_{\theta}(\tau)\\&=\sum_{\tau} R(\tau) p_{\theta}(\tau) \frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)} \\&= \sum_{\tau} R(\tau) p_{\theta}(\tau) \nabla \log p_{\theta}(\tau) \\ &=E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] \end{aligned} Rˉθ=τR(τ)pθ(τ)=τR(τ)pθ(τ)pθ(τ)pθ(τ)=τR(τ)pθ(τ)logpθ(τ)=Eτpθ(τ)[R(τ)logpθ(τ)]
最终是一个期望,这个期望无法直接计算,只能通过sample一些轨迹 τ \tau τ, 求解器平均值来计算梯度,有了梯度以后就可以更新参数,具体公式如下:
E τ ∼ p θ ( τ ) [ R ( τ ) ∇ log ⁡ p θ ( τ ) ] ≈ 1 N ∑ n = 1 N R ( τ n ) ∇ log ⁡ p θ ( τ n ) = 1 N ∑ n = 1 N ∑ t = 1 T n R ( τ n ) ∇ log ⁡ p θ ( a t n ∣ s t n ) \begin{aligned} E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] &\approx \frac{1}{N} \sum_{n=1}^{N} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(\tau^{n}\right) \\ &=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) \end{aligned} Eτpθ(τ)[R(τ)logpθ(τ)]N1n=1NR(τn)logpθ(τn)=N1n=1Nt=1TnR(τn)logpθ(atnstn)

注: p θ ( τ ) p_{\theta}(\tau) pθ(τ) 里面有两项, p ( s t + 1 ∣ s t , a t ) p(s_{t+1}|s_t,a_t) p(st+1st,at) 来自于 environment, p θ ( a t ∣ s t ) p_\theta(a_t|s_t) pθ(atst) 是来自于 agent。

p ( s t + 1 ∣ s t , a t ) p(s_{t+1}|s_t,a_t) p(st+1st,at) 由环境决定从而与 θ \theta θ 无关,因此 ∇ log ⁡ p ( s t + 1 ∣ s t , a t ) = 0 \nabla \log p(s_{t+1}|s_t,a_t) =0 logp(st+1st,at)=0。因此 ∇ p θ ( τ ) = ∇ log ⁡ p θ ( a t n ∣ s t n ) \nabla p_{\theta}(\tau)= \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right) pθ(τ)=logpθ(atnstn)

然后对推导得到的最终结果进行定性解释:

  • 在sample到的轨迹中,某一个状态 s t s_t st, 要执行动作 a t a_t at

  • 如果 R ( τ ) R(\tau) R(τ)是正的,那就要增加 ( s t , a t ) (s_t,a_t) (st,at)的概率,让actor能够在下一次遇到 s t s_t st以后能以更高的概率选中 a t a_t at

  • 为负同理。

4. 实现/实做

具体实现过程如下:
θ ← θ + η ∇ R θ ˉ ∇ R θ ˉ = 1 N ∑ n = 1 N ∑ t = 1 T n R ( τ n ) ∇ l o g p θ ( a t n ∣ s t n ) \theta \leftarrow \theta+\eta \nabla \bar{R_\theta} \\ \nabla \bar{R_\theta}=\frac{1}{N}\sum^N_{n=1}\sum^{T_n}_{t=1}R(\tau^n)\nabla log p_\theta(a_t^n|s_t^n) θθ+ηRθˉRθˉ=N1n=1Nt=1TnR(τn)logpθ(atnstn)
还需要收集一系列s和a的pair,以及对应的reward。将sample得到的s和a组成的pair带入到上面的gradient的式子中,然后计算其log probablitiy,然后取gradient,然后就可以进行更新了。

这一点和分类问题中的交叉熵有一点类似,可以按照以下方法进行理解:
标签值 × l o g ( 标签对应的概率 ) \text{标签值}\times log(\text{标签对应的概率}) 标签值×log(标签对应的概率)
这样就和以上做到了形式上的一致(不是很严谨)。RL和一般分类问题不同的地方是loss前面乘上了weight R。

在这里会需要乘以一个奖励回报 R R R。这个奖励回报相当于是对这个真实 action 的评价, R R R 具体越大,未来总收益越大,说明当前输出的这个真实的 action 就越好,这个 loss 就越需要重视。如果 R R R 越小,那就说明做这个 action a t a_t at 并没有那么的好,loss 的权重就要小一点,优化力度就小一点。

4.1 TIP1 Add a Baseline

θ ← θ + η ∇ R θ ˉ ∇ R θ ˉ = 1 N ∑ n = 1 N ∑ t = 1 T n ( R ( τ n ) − b ) ∇ l o g p θ ( a t n ∣ s t n ) b ≈ E [ R ( τ ) ] \theta \leftarrow \theta+\eta \nabla \bar{R_\theta} \\ \nabla \bar{R_\theta}=\frac{1}{N}\sum^N_{n=1}\sum^{T_n}_{t=1}(R(\tau^n)-b)\nabla log p_\theta(a_t^n|s_t^n) \\ b \approx E[R(\tau)] θθ+ηRθˉRθˉ=N1n=1Nt=1Tn(R(τn)b)logpθ(atnstn)bE[R(τ)]

如果某些样本没有sample到,那其他动作的概率都会提升,它本身概率会下降,这就存在问题了,可以通过添加一个baseline,让reward不总是正的值。

4.2 TIP2 Assign Suitable Credit

下面这个式子的话,

∇ R ˉ θ ≈ 1 N ∑ n = 1 N ∑ t = 1 T n ( R ( τ n ) − b ) ∇ log ⁡ p θ ( a t n ∣ s t n ) \nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(R\left(\tau^{n}\right)-b\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) RˉθN1n=1Nt=1Tn(R(τn)b)logpθ(atnstn)

原来会做的事情是,在某一个 state,假设你执行了某一个 action a,它得到的 reward ,它前面乘上的这一项 R ( τ n ) − b R(\tau^n)-b R(τn)b, 这个值就可以理解为,当前s下使用动作a以后的好坏程度。

R ( τ n ) R(\tau^n) R(τn) 代表整个episode执行完以后得到的结果,由于强化学习具有延迟奖励的特点,可以考虑以下改进:更关注于近期得到的奖励,长远奖励要被削弱。这个想法实现如下:

∇ R ˉ θ ≈ 1 N ∑ n = 1 N ∑ t = 1 T n ( ∑ t ′ = t T n γ t ′ − t r t ′ n − b ) ∇ log ⁡ p θ ( a t n ∣ s t n ) \nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(\sum_{t'=t}^{T_n}\gamma^{t'-t}r_{t'}^n-b\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) RˉθN1n=1Nt=1Tn(t=tTnγttrtnb)logpθ(atnstn)

其中 γ \gamma γ代表的是0-1之间的小数,用于削弱长远的奖励,这就是discount fastor:

  • 一般会设个 0.9 或 0.99,
  • γ = 0 \gamma = 0 γ=0 : 只关心即时奖励;
  • γ = 1 \gamma = 1 γ=1 : 未来奖励等同于即时奖励。

然后就可以顺利引入Advantage Function,这个函数是依赖于状态s和动作a的,如下所示:

A θ ( s t , a t ) A^\theta(s_t,a_t) Aθ(st,at)

代表在 s t s_t st状态下执行动作 a t a_t at到底有多好,可以带来多大的奖励。这个”好“代表的是相对优势,因为会减掉baseline,这个函数通常可以是由一个network(critic)估计出来的。

5. MC & TD

策略梯度可以用蒙特卡洛算法(MC)或者时序差分算法(TD)求解。

5.1 MC-REINFORCE

蒙特卡洛算法是回合更新,常见算法是REINFORCE。

∇ R ˉ θ ≈ 1 N ∑ n = 1 N ∑ t = 1 T n G t n ∇ log ⁡ p θ ( a t n ∣ s t n ) \nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}G_t^n \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) RˉθN1n=1Nt=1TnGtnlogpθ(atnstn)

可以理解为完成一个episode以后,拿这个episode的数据去学习,然后做一次更新。

G t G_t Gt 是未来总收益, G t G_t Gt 代表是从这个 step 后面,能拿到的收益之和是多少。

在代码上的处理上是先拿到每个 step 的 reward,然后计算每个 step 的未来总收益 G t G_t Gt 是多少,然后拿每个 G t G_t Gt 代入公式,去优化每一个 action 的输出。

编写代码时会有这样一个函数,输入每个 step 拿到的 reward,把这些 reward 转成每一个 step 的未来总收益。因为未来总收益是这样计算的:
G t = ∑ k = t + 1 T γ k − t − 1 r k = r t + 1 + γ G t + 1 \begin{aligned} G_{t} &=\sum_{k=t+1}^{T} \gamma^{k-t-1} r_{k} \\ &=r_{t+1}+\gamma G_{t+1} \end{aligned} Gt=k=t+1Tγkt1rk=rt+1+γGt+1

先产生一个 episode 的数据,比如 ( s 1 , a 1 , G 1 ) , ( s 2 , a 2 , G 2 ) , ⋯   , ( s T , a T , G T ) (s_1,a_1,G_1),(s_2,a_2,G_2),\cdots,(s_T,a_T,G_T) (s1,a1,G1),(s2,a2,G2),,(sT,aT,GT)。然后针对每个 action 来计算梯度。

在代码上计算时,要拿到神经网络的输出。神经网络会输出每个 action 对应的概率值,然后还可以拿到实际的 action,把它转成 one-hot 向量乘一下,可以拿到 ln ⁡ π ( A t ∣ S t , θ ) \ln \pi(A_t|S_t,\theta) lnπ(AtSt,θ)

REINFORCE流程图

5.2 TD-Actor Critic

时序差分是单步更新,更新频率更高,这里用Q-function来近似表示未来的收益。

∇ R ˉ θ ≈ 1 N ∑ n = 1 N ∑ t = 1 T n Q n ( s t n , a t n ) ∇ log ⁡ p θ ( a t n ∣ s t n ) \nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}Q^n(s_t^n,a^n_t) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) RˉθN1n=1Nt=1TnQn(stn,atn)logpθ(atnstn)

时序差分强化学习能够在知道结果之前就开始学习,相比蒙特卡洛强化学习,其更快速、灵活。

6. 问答

Q: 在一个process中,一个具体的trajectory s 1 s_1 s1, a 1 a_1 a1, s 2 s_2 s2 , a 2 a_2 a2 出现的概率取决于什么?

A:

  1. 一部分是 environment 的行为, environment 的 function 它内部的参数或内部的规则长什么样子。 p ( s t + 1 ∣ s t , a t ) p(s_{t+1}|s_t,a_t) p(st+1st,at)这一项代表的是 environment, environment 这一项通常你是无法控制它的,因为那个是人家写好的,或者已经客观存在的。
  2. 另一部分是 agent 的行为,你能控制的是 p θ ( a t ∣ s t ) p_\theta(a_t|s_t) pθ(atst)。给定一个 s t s_t st, actor 要采取什么样的 a t a_t at 会取决于你 actor 的参数 θ \theta θ, 所以这部分是 actor 可以自己控制的。随着 actor 的行为不同,每个同样的 trajectory, 它就会有不同的出现的概率。

Q: 可以使用哪些方法来进行gradient ascent的计算?

A:用 gradient ascent 来 update 参数,对于原来的参数 θ \theta θ ,可以将原始的 θ \theta θ 加上更新的 gradient 这一项,再乘以一个 learning rate,learning rate 其实也是要调的,和神经网络一样,可以使用 Adam、RMSProp 等优化器对其进行调整。


Q: Advantage Function作用:

A: 在某一个 state s t s_t st 执行某一个 action a t a_t at,相较于其他可能的 action,它有多好。它在意的不是一个绝对的好,而是相对的好,即相对优势(relative advantage)。因为会减掉一个 b,减掉一个 baseline, 所以这个东西是相对的好,不是绝对的好。 A θ ( s t , a t ) A^{\theta}\left(s_{t}, a_{t}\right) Aθ(st,at) 通常可以是由一个 network estimate 出来的,这个 network 叫做 critic。


Q:对于梯度策略的两种方法,蒙特卡洛(MC)强化学习和时序差分(TD)强化学习两个方法有什么联系和区别

A: 两者的更新频率不同,蒙特卡洛强化学习方法是每一个episode更新一次,即需要经历完整的状态序列后再更新(比如贪吃蛇游戏,贪吃蛇“死了”游戏结束后再更新)

对于时序差分强化学习方法是每一个step就更新一次 ,(比如贪吃蛇游戏,贪吃蛇每移动一次(或几次)就进行更新)。相对来说,时序差分强化学习方法比蒙特卡洛强化学习方法更新的频率更快。

时序差分强化学习能够在知道一个小step后就进行学习,相比于蒙特卡洛强化学习,其更加快速、灵活

7. 参考


http://www.niftyadmin.cn/n/1003477.html

相关文章

Kinect for windows语音识别(Speech)

Kinect for windows提供了语音识别的能力,它靠Kinect的语音采集流进行识别的,这是建立在微软的语音识虽库的基础上的,关于微软语音识别可以参考http://msdn.microsoft.com/en-us/library/hh361572(voffice.14).aspx。对别Kinect识别的语音&am…

Android入门第九篇之AlertDialog

时隔一年,又要准备做Android的开发了,最近复习和整理一下Android的知识。 这次要说的是AlertDialog,这种对话框会经常遇到。AlertDialog跟WIN32开发中的Dialog不一样,AlertDialog是非阻塞的,而阻塞的对话框用的是Popu…

.NET 性能优化方法总结==转

.NET 性能优化方法总结 目录 目录 1. C#语言方面... 4 1.1 垃圾回收... 4 1.1.1 避免不必要的对象创建... 4 1.1.2 不要使用空析构函数 ★... 4 1.1.3 实现 IDisposable 接口... 4 1.2 String 操作... 5 1.2.1 使用 StringBuilder 做字符串连接... 5 1.2.2 避免不必要的调用 To…

Android入门第十篇之PopupWindow

介绍过AlertDialog之后,接下来就介绍一下PopupWindow这种对话框。PopupWindow是阻塞对话框,只有在外部线程 或者 PopupWindow本身做退出操作才行。PopupWindow完全依赖Layout做外观,在常见的开发中,PopupWindow应该会与 AlertDial…

摘:C语言数字转换为字符串

数字转换为字符串 C语言提供了几个标准库函数&#xff0c;可以将任意类型(整型、长整型、浮点型等)的数字转换为字符串。以下是用itoa()函数将整数转换为字符串的一个例子&#xff1a; # include <stdio. h># include <stdlib. h> void main (void);void main (vo…

Android入门第十一篇之TabHost,TabWidget

这回要介绍的是Android的Tab控件&#xff0c;Tab控件可以达到分页的效果&#xff0c;让一个屏幕的内容尽量丰富&#xff0c;当然也会增加开发的复杂程度&#xff0c;在有必要的时候再使用。Android的Tab控件使用起来有点奇怪&#xff0c;必须包含和按照以下的顺序&#xff1a; …

SRM 585 DIV2

250pt&#xff1a; 一水... 500pt:题意: 给你一颗满二叉树的高度&#xff0c;然后找出出最少的不想交的路径并且该路径每个节点只经过一次。 思路&#xff1a;观察题目中给的图就会发现&#xff0c;其实每形成一个 就会存在一条路径。 我们只要求该满二叉树一共包含多少个即可。…