DP на строках
Задача 1: Longest Common Subsequence (LCS)
Условие: найти длину наибольшей общей подпоследовательности двух строк.
s1 = "abcde"
s2 = "ace"
LCS = "ace", длина 3
Рекуррентность
Если s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # Символы совпали
Иначе:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # Пропускаем один из символов
<?php
declare(strict_types=1);
function longestCommonSubsequence(string $s1, string $s2): int
{
$m = strlen($s1);
$n = strlen($s2);
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = 1; $i <= $m; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($s1[$i - 1] === $s2[$j - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
} else {
$dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]);
}
}
}
return $dp[$m][$n];
}
package main
func longestCommonSubsequence(s1, s2 string) int {
m, n := len(s1), len(s2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s1[i-1] == s2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
return dp[m][n]
}
"" a c e
"" 0 0 0 0
a 0 1 1 1
b 0 1 1 1
c 0 1 2 2
d 0 1 2 2
e 0 1 2 3 <- ответ
Восстановление LCS
<?php
declare(strict_types=1);
function getLcs(string $s1, string $s2): string
{
$m = strlen($s1);
$n = strlen($s2);
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = 1; $i <= $m; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($s1[$i - 1] === $s2[$j - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
} else {
$dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]);
}
}
}
// Reconstruct LCS
$lcs = '';
$i = $m;
$j = $n;
while ($i > 0 && $j > 0) {
if ($s1[$i - 1] === $s2[$j - 1]) {
$lcs = $s1[$i - 1] . $lcs;
$i--;
$j--;
} elseif ($dp[$i - 1][$j] > $dp[$i][$j - 1]) {
$i--;
} else {
$j--;
}
}
return $lcs;
}
package main
func getLcs(s1, s2 string) string {
m, n := len(s1), len(s2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s1[i-1] == s2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
// Reconstruct LCS
var lcs []byte
i, j := m, n
for i > 0 && j > 0 {
if s1[i-1] == s2[j-1] {
lcs = append(lcs, s1[i-1])
i--
j--
} else if dp[i-1][j] > dp[i][j-1] {
i--
} else {
j--
}
}
// Reverse the result
for l, r := 0, len(lcs)-1; l < r; l, r = l+1, r-1 {
lcs[l], lcs[r] = lcs[r], lcs[l]
}
return string(lcs)
}
Условие: минимальное количество операций (вставка, удаление, замена) для преобразования s1 в s2.
"horse" -> "ros"
horse -> rorse (замена h->r)
rorse -> rose (удаление r)
rose -> ros (удаление e)
Ответ: 3
Рекуррентность
Если s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] # Символы равны, ничего не делаем
Иначе:
dp[i][j] = 1 + min(
dp[i-1][j], # Удаление из s1
dp[i][j-1], # Вставка в s1
dp[i-1][j-1] # Замена
)
<?php
declare(strict_types=1);
function minDistance(string $word1, string $word2): int
{
$m = strlen($word1);
$n = strlen($word2);
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
// Base cases
for ($i = 0; $i <= $m; $i++) {
$dp[$i][0] = $i; // Delete all characters from word1
}
for ($j = 0; $j <= $n; $j++) {
$dp[0][$j] = $j; // Insert all characters of word2
}
for ($i = 1; $i <= $m; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($word1[$i - 1] === $word2[$j - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1];
} else {
$dp[$i][$j] = 1 + min(
$dp[$i - 1][$j], // delete
$dp[$i][$j - 1], // insert
$dp[$i - 1][$j - 1] // replace
);
}
}
}
return $dp[$m][$n];
}
package main
func minDistance(word1, word2 string) int {
m, n := len(word1), len(word2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// Base cases
for i := 0; i <= m; i++ {
dp[i][0] = i // Delete all characters from word1
}
for j := 0; j <= n; j++ {
dp[0][j] = j // Insert all characters of word2
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = 1 + min(
dp[i-1][j], // delete
dp[i][j-1], // insert
dp[i-1][j-1], // replace
)
}
}
}
return dp[m][n]
}
"" r o s
"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3 <- ответ
Задача 3: Longest Palindromic Subsequence
<?php
declare(strict_types=1);
function longestPalindromeSubseq(string $s): int
{
$n = strlen($s);
// dp[i][j] = length of LPS in s[i..j]
$dp = array_fill(0, $n, array_fill(0, $n, 0));
for ($i = 0; $i < $n; $i++) {
$dp[$i][$i] = 1; // Each character is a palindrome of length 1
}
for ($length = 2; $length <= $n; $length++) {
for ($i = 0; $i <= $n - $length; $i++) {
$j = $i + $length - 1;
if ($s[$i] === $s[$j]) {
$dp[$i][$j] = $dp[$i + 1][$j - 1] + 2;
} else {
$dp[$i][$j] = max($dp[$i + 1][$j], $dp[$i][$j - 1]);
}
}
}
return $dp[0][$n - 1];
}
// s = "bbbab"
// LPS = "bbbb", length 4
package main
func longestPalindromeSubseq(s string) int {
n := len(s)
// dp[i][j] = length of LPS in s[i..j]
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
dp[i][i] = 1 // Each character is a palindrome of length 1
}
for length := 2; length <= n; length++ {
for i := 0; i <= n-length; i++ {
j := i + length - 1
if s[i] == s[j] {
dp[i][j] = dp[i+1][j-1] + 2
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
}
}
}
return dp[0][n-1]
}
// s = "bbbab"
// LPS = "bbbb", length 4
Задача 4: Longest Palindromic Substring
<?php
declare(strict_types=1);
function longestPalindrome(string $s): string
{
$n = strlen($s);
if ($n < 2) {
return $s;
}
$start = 0;
$maxLen = 1;
// dp[i][j] = true if s[i..j] is a palindrome
$dp = array_fill(0, $n, array_fill(0, $n, false));
for ($i = 0; $i < $n; $i++) {
$dp[$i][$i] = true;
}
for ($i = 0; $i < $n - 1; $i++) {
if ($s[$i] === $s[$i + 1]) {
$dp[$i][$i + 1] = true;
$start = $i;
$maxLen = 2;
}
}
for ($length = 3; $length <= $n; $length++) {
for ($i = 0; $i <= $n - $length; $i++) {
$j = $i + $length - 1;
if ($s[$i] === $s[$j] && $dp[$i + 1][$j - 1]) {
$dp[$i][$j] = true;
if ($length > $maxLen) {
$start = $i;
$maxLen = $length;
}
}
}
}
return substr($s, $start, $maxLen);
}
package main
func longestPalindrome(s string) string {
n := len(s)
if n < 2 {
return s
}
start, maxLen := 0, 1
// dp[i][j] = true if s[i..j] is a palindrome
dp := make([][]bool, n)
for i := range dp {
dp[i] = make([]bool, n)
dp[i][i] = true
}
for i := 0; i < n-1; i++ {
if s[i] == s[i+1] {
dp[i][i+1] = true
start = i
maxLen = 2
}
}
for length := 3; length <= n; length++ {
for i := 0; i <= n-length; i++ {
j := i + length - 1
if s[i] == s[j] && dp[i+1][j-1] {
dp[i][j] = true
if length > maxLen {
start = i
maxLen = length
}
}
}
}
return s[start : start+maxLen]
}
<?php
declare(strict_types=1);
function longestPalindromeExpand(string $s): string
{
$expand = function (int $left, int $right) use ($s): string {
$len = strlen($s);
while ($left >= 0 && $right < $len && $s[$left] === $s[$right]) {
$left--;
$right++;
}
return substr($s, $left + 1, $right - $left - 1);
};
$result = '';
for ($i = 0; $i < strlen($s); $i++) {
$odd = $expand($i, $i); // Odd length
$even = $expand($i, $i + 1); // Even length
if (strlen($odd) > strlen($result)) {
$result = $odd;
}
if (strlen($even) > strlen($result)) {
$result = $even;
}
}
return $result;
}
package main
func longestPalindromeExpand(s string) string {
expand := func(left, right int) string {
for left >= 0 && right < len(s) && s[left] == s[right] {
left--
right++
}
return s[left+1 : right]
}
result := ""
for i := 0; i < len(s); i++ {
odd := expand(i, i) // Odd length
even := expand(i, i+1) // Even length
if len(odd) > len(result) {
result = odd
}
if len(even) > len(result) {
result = even
}
}
return result
}
<?php
declare(strict_types=1);
function isMatch(string $s, string $p): bool
{
$m = strlen($s);
$n = strlen($p);
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, false));
$dp[0][0] = true;
// Base case: a* can match empty string
for ($j = 1; $j <= $n; $j++) {
if ($p[$j - 1] === '*') {
$dp[0][$j] = $dp[0][$j - 2];
}
}
for ($i = 1; $i <= $m; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($p[$j - 1] === '.' || $p[$j - 1] === $s[$i - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1];
} elseif ($p[$j - 1] === '*') {
$dp[$i][$j] = $dp[$i][$j - 2]; // Zero occurrences
if ($p[$j - 2] === '.' || $p[$j - 2] === $s[$i - 1]) {
$dp[$i][$j] = $dp[$i][$j] || $dp[$i - 1][$j]; // One+ occurrences
}
}
}
}
return $dp[$m][$n];
}
package main
func isMatch(s, p string) bool {
m, n := len(s), len(p)
dp := make([][]bool, m+1)
for i := range dp {
dp[i] = make([]bool, n+1)
}
dp[0][0] = true
// Base case: a* can match empty string
for j := 1; j <= n; j++ {
if p[j-1] == '*' {
dp[0][j] = dp[0][j-2]
}
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if p[j-1] == '.' || p[j-1] == s[i-1] {
dp[i][j] = dp[i-1][j-1]
} else if p[j-1] == '*' {
dp[i][j] = dp[i][j-2] // Zero occurrences
if p[j-2] == '.' || p[j-2] == s[i-1] {
dp[i][j] = dp[i][j] || dp[i-1][j] // One+ occurrences
}
}
}
}
return dp[m][n]
}
| Задача | Сложность | Подход |
|---|---|---|
| LCS | O(m*n) | 2D DP, совпадение символов |
| Edit Distance | O(m*n) | 2D DP, min из 3 операций |
| Longest Palindromic Subseq | O(n²) | 2D DP, интервалы |
| Longest Palindromic Substring | O(n²) / O(n) | DP или expand around center |
| Regular Expression | O(m*n) | 2D DP с обработкой * и . |
| Wildcard Matching | O(m*n) | 2D DP с обработкой * и ? |
Запомни: DP на строках почти всегда использует 2D таблицу dp[i][j] где i и j — позиции в двух строках. Заполнение идёт по строкам или по длине подстроки. LCS и Edit Distance — две базовые задачи, из которых выводятся многие другие. Palindrome задачи можно свести к LCS(s, reverse(s)).
Итоги
- LCS: dp[i][j] = dp[i-1][j-1]+1 при совпадении, иначе max
- Edit Distance: dp[i][j] = 1 + min(delete, insert, replace)
- Palindrome Subseq = LCS(s, reverse(s))
- Palindrome Substring: expand around center O(n^2) проще DP
- Все задачи: O(m*n) или O(n^2), можно оптимизировать память до O(min(m,n))