Guia de entrevista sobre algoritmo e estrutura de dados - explicação detalhada do algoritmo de programação dinâmica

Características do problema de programação dinâmica

Aprendemos como a programação dinâmica resolve problemas decompondo-os em subproblemas. Na verdade, a decomposição de subproblemas é uma ideia algorítmica geral, com diferentes ênfases em dividir e conquistar, programação dinâmica e retrocesso.

  • O algoritmo de divisão e conquista divide recursivamente o problema original em vários subproblemas independentes até que o menor subproblema seja alcançado e mescla as soluções dos subproblemas em retrocesso para finalmente obter a solução para o problema original.
  • A programação dinâmica também decompõe recursivamente o problema, mas a principal diferença do algoritmo de divisão e conquista é que os subproblemas na programação dinâmica são interdependentes e muitos subproblemas sobrepostos aparecerão durante o processo de decomposição.
  • O algoritmo de retrocesso esgota todas as soluções possíveis em tentativas e regressões e evita ramificações de pesquisa desnecessárias por meio de poda. A solução do problema original consiste em uma série de etapas de decisão, e podemos considerar a subsequência antes de cada etapa de decisão como um subproblema.

Na verdade, a programação dinâmica é frequentemente usada para resolver problemas de otimização, que não apenas contêm subproblemas sobrepostos, mas também possuem duas outras características: subestrutura ideal e sem efeitos posteriores.

subestrutura ideal

Modificamos ligeiramente o problema de subir escadas para torná-lo mais adequado à demonstração do conceito de subestrutura ótima.

!!! pergunta "O menor custo para subir escadas"

给定一个楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 个台阶需要付出的代价,$cost[0]$ 为地面起始点。请计算最少需要付出多少代价才能到达顶部?

Como mostrado na figura abaixo, se 1 11 ,2 22 ,3 3Os custos do nível 3 são respectivamente1 11 ,10 1010 ,1 11 , depois suba do chão até o 3º3O custo mínimo do pedido 3 é2 22

Insira a descrição da imagem aqui

dp [ eu ] dp[i]d p ​​​​[ i ] significa subir até oiésimoO preço cumulativo pago no nível i , devido ao custo no nível iiA ordem i só é possível dei − 1 i - 1eu- ordem oui − 2 i − 2eu-A segunda ordem chega, entãodp[i]dp[i]d p ​​​​[ i ] só pode ser igual adp [ i − 1 ] + custo [ i ] dp [i - 1] + custo [i]d p ​​[ eu-1 ]+custo t [ i ]dp [ i − 2 ] + custo [ i ] dp[i - 2] + custo[i]d p ​​[ eu-2 ]+custo [ eu ] . _ Para minimizar o custo, devemos escolher o menor dos dois:

dp [ i ] = min ⁡ ( dp [ i − 1 ] , dp [ i − 2 ] ) + custo [ i ] dp[i] = \min(dp[i-1], dp[i-2]) + custo[eu]d p ​​[ eu ]=min ( d p [ eu-1 ] ,d p ​​[ eu-2 ])+custo [ eu ] _

Isto pode levar ao significado de subestrutura ótima: a solução ótima para o problema original é construída a partir das soluções ótimas para os subproblemas .

Este problema obviamente tem uma subestrutura ótima: resolvemos de forma otimizada os dois subproblemas dp [ i − 1 ] dp[i-1]d p ​​[ eu-1 ]dp [i - 2] dp[i-2]d p ​​[ eu-2 ] e use-o para construir o problema originaldp[i]dp[i]A solução ótima de d p [ i ] .

Então, o problema de subir escadas da seção anterior tem uma subestrutura ideal? Seu objetivo é resolver o número de soluções. Parece ser um problema de contagem, mas se você perguntar de outra forma: “Resolva o número máximo de soluções”. Descobrimos inesperadamente que, embora a questão fosse equivalente antes e depois da modificação, surgiu a subestrutura ideal : Não .O número máximo de soluções de ordem n é igual a n − 1 n-1n- ordem e enésima− 2 n-2n-A soma do número máximo de soluções de ordem 2 . Portanto, o método de explicação da subestrutura ótima é relativamente flexível e terá significados diferentes em problemas diferentes.

De acordo com a equação de transição de estado, e o estado inicial dp [1] = custo [1] dp[1] = custo[1]d p ​​[ 1 ]=custo t [ 1 ]dp [ 2 ] = custo [ 2 ] dp[2] = custo[2]d p ​​[ 2 ]=cos t [ 2 ] , podemos obter o código de programação dinâmica.

=== “Píton”

```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```

=== “C++”

```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Java”

```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```

=== “C#”

```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```

=== “Vá”

```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Rápido”

```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “JS”

```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “TS”

```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Dardo”

```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Ferrugem”

```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```

=== “C”

```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Zig”

```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDP}
```

A figura abaixo mostra o processo de programação dinâmica do código acima.

Insira a descrição da imagem aqui

Este problema também pode ser otimizado em termos de espaço, comprimindo uma dimensão em dimensão zero, de modo que a complexidade do espaço mude de O (n) O (n)O ( n ) DescendênciaO (1) O (1)O ( 1 )

=== “Píton”

```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```

=== “C++”

```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Java”

```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```

=== “C#”

```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```

=== “Vá”

```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Rápido”

```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “JS”

```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “TS”

```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Dardo”

```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Ferrugem”

```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```

=== “C”

```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Zig”

```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

sem efeitos colaterais

A ausência de efeitos posteriores é uma das características importantes da programação dinâmica que pode resolver problemas de forma eficaz.É definida como: dado um determinado estado, seu desenvolvimento futuro está relacionado apenas ao estado atual e não tem nada a ver com todos os estados que o atual estado experimentou no passado .

Tomando como exemplo o problema de subir escadas, dado o estado iii , desenvolverá o estadoi + 1 i+1eu+1 e estadoi + 2 i+2eu+2 , correspondendo ao salto1 11 passo e salto2 22 etapas. Ao fazer estas duas escolhas, não precisamos considerar o estadoiiO estado antes de i , sua contribuição para o estado iio futuro de i não tem impacto.

Contudo, se adicionarmos uma restrição ao problema de subir escadas, a situação é diferente.

!!! pergunta “Subir escadas com restrições”

给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,**但不能连续两轮跳 $1$ 阶**,请问有多少种方案可以爬到楼顶。

Por exemplo, na imagem abaixo, suba para o 3º lugarRestam apenas 2 2no nível 32 soluções possíveis, incluindo três saltos consecutivos1 1A solução de primeira ordem não satisfaz as restrições e, portanto, é descartada.

Insira a descrição da imagem aqui

Neste problema, se a rodada anterior foi o salto 1 1Se você chegar ao nível 1 , deverá pular2 2Nível 2 . Isto significa quea escolha do próximo passo não pode ser determinada independentemente do estado actual (o número actual de escadas), mas também está relacionada com o estado anterior (o número de escadas na ronda anterior).

Não é difícil descobrir que este problema não satisfaz mais nenhum efeito posterior.A equação de transição de estado dp [i] = dp [i - 1] + dp [i - 2] dp[i] = dp[i-1] + dp [eu -2]d p ​​[ eu ]=d p ​​[ eu-1 ]+d p ​​[ eu-2 ] também falha porquedp [i - 1] dp[i-1]d p ​​[ eu-1 ] representa o salto1 11 nível, mas contém muitos "saltos da rodada anterior1 1Esquema de 1 ordem para cima" e, para satisfazer as restrições, não podemos alterardp [i - 1] dp[i-1]d p ​​[ eu-1 ] Incluído diretamente emdp[i]dp[i]dp [ i ]

Para fazer isso, precisamos estender a definição de estado: estado [i, j] [i, j][ eu ,j ] indica que está noiésimoNível i , e saltoujjordem j , ondej ∈ { 1 , 2 } j \in \{1, 2\}j{ 1 ,2 } . Esta definição de estado distingue efetivamente a rodada anterior de saltos1 1Nível 1 ounível 2 2Nível 2 , podemos usar isso para decidir como saltar a seguir.

  • Quando jjj é igual a1 11 , ou seja, a última rodada saltou1 1No nível 1 , você só pode optar por pular 2 2nesta rodada.Nível 2 .
  • Quando jjj é iguala 2 22 , ou seja, a rodada anterior saltou2 2No nível 2 , você pode optar por pular1 1Nível 1 ou salto2 2Nível 2 .

Conforme mostrado na figura abaixo, sob esta definição, dp [ i , j ] dp[i, j]d p ​​[ eu ,j ] representa o estado[i, j] [i, j][ eu ,j ] correspondente ao número de planos. Neste momento, a equação de transição de estado é:

{ dp [ i , 1 ] = dp [ i − 1 , 2 ] dp [ i , 2 ] = dp [ i − 2 , 1 ] + dp [ i − 2 , 2 ] \begin{casos} dp[i, 1 ] = dp[i-1, 2] \\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \end{casos}{ d p ​​[ eu ,1 ]=d p ​​[ eu-1 ,2 ]d p ​​[ eu ,2 ]=d p ​​[ eu-2 ,1 ]+d p ​​[ eu-2 ,2 ]

Insira a descrição da imagem aqui

Finalmente, retorne dp [ n , 1 ] + dp [ n , 2 ] dp[n, 1] + dp[n, 2]d p ​​[ n ,1 ]+d p ​​[ n ,2 ] , a soma dos dois representa subir até oenésimoO número total de soluções de ordem n .

=== “Píton”

```python title="climbing_stairs_constraint_dp.py"
[class]{}-[func]{climbing_stairs_constraint_dp}
```

=== “C++”

```cpp title="climbing_stairs_constraint_dp.cpp"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Java”

```java title="climbing_stairs_constraint_dp.java"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```

=== “C#”

```csharp title="climbing_stairs_constraint_dp.cs"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```

=== “Vá”

```go title="climbing_stairs_constraint_dp.go"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Rápido”

```swift title="climbing_stairs_constraint_dp.swift"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “JS”

```javascript title="climbing_stairs_constraint_dp.js"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “TS”

```typescript title="climbing_stairs_constraint_dp.ts"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Dardo”

```dart title="climbing_stairs_constraint_dp.dart"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Ferrugem”

```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
```

=== “C”

```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Zig”

```zig title="climbing_stairs_constraint_dp.zig"
[class]{}-[func]{climbingStairsConstraintDP}
```

No caso acima, como precisamos apenas considerar o estado anterior, ainda podemos fazer com que o problema não satisfaça novamente quaisquer efeitos posteriores, estendendo a definição do estado. No entanto, alguns problemas têm “repercussões” muito sérias.

!!! pergunta "Subida de escadas e geração de obstáculos"

给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2$、$3$ 阶上,则之后就不能跳到第 $4$、$6$ 阶上。请问有多少种方案可以爬到楼顶。

Neste problema, o próximo salto depende de todos os estados passados, porque cada salto cria obstáculos numa escada mais alta e afeta saltos futuros. Para este tipo de problema, a programação dinâmica costuma ser difícil de resolver.

Na verdade, muitos problemas complexos de otimização combinatória (como o problema do caixeiro viajante) não satisfazem a propriedade livre de efeitos posteriores. Para este tipo de problema, normalmente optamos por utilizar outros métodos, como busca heurística, algoritmo genético, aprendizagem por reforço, etc., para obter soluções ótimas locais disponíveis em um tempo limitado.

Acho que você gosta

Origin blog.csdn.net/zy_dreamer/article/details/132923057
Recomendado
Clasificación