滑动窗口法+最小覆盖子串+常规解法及优化

本文介绍了使用滑动窗口法解决寻找字符串中最小覆盖子串的问题。首先阐述了常规解法,包括从左往右寻找目标字符并记录出现次数,然后尝试缩小窗口检查目标字符是否达到预期。接着讨论了优化点,提出预处理字符串以忽略未出现在目标字符串中的字符,并解决了在处理过程中可能遇到的特殊情况。最后给出了优化后的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

滑动窗口法之最小覆盖子串


常规解法

先扩大窗口:从左往右找目标字符并记录出现次数。
再缩小窗口:每找到一次目标字符即尝试缩小,看目标字符是否都出现了一次,且记录的字符出现次数大于目标字符应出现次数。

代码

function minWindow($s, $t)
{
    // $left、$right 窗口左右边界值
    $left = $right = 0;
    // $start、$len 最小覆盖子串的开始位置及长度
    $start = -1;
    $len = PHP_INT_MAX;
    // $target、$window 目标子串的各字符出现次数、当前窗口的
    $target = $window = [];
    $tLen = strlen($t);
    for ($i = 0; $i < $tLen; $i++) {
        $target[$t[$i]] = isset($target[$t[$i]]) ? $target[$t[$i]] + 1 : 1;
    }
    // 按目标字符出现顺序进行比较,当目标字符都按顺序出现才返回真
    $check = function (&$target, &$window) {
        foreach ($target as $k => $v) {
            if (!isset($window[$k]) || $window[$k] < $v) {
                return false;
            }
        }
        return true;
    };

    $sLen = strlen($s);
    while ($right < $sLen) {
        if (isset($target[$s[$right]])) {
            $window[$s[$right]] = isset($window[$s[$right]]) ? $window[$s[$right]] + 1 : 1;
        }

        while ($check($target, $window) && $left <= $right) {
            if ($right - $left + 1 < $len) {
                $len = $right - $left + 1;
                $start = $left;
            }
            if (isset($window[$s[$left]])) {
                --$window[$s[$left]];
            }
            ++$left;
        }
        ++$right;
    }

    return $start == -1 ? '' : substr($s, $start, $len);
}

// 测试
echo minWindow('ADOBECODEBANC', 'ABC'), "\n";
// BANC
echo minWindow('a', 'a'), "\n";
// a
echo minWindow('a', 'b'), "\n";
// 无
echo minWindow('ab', 'b'), "\n";
// b
echo minWindow('abc', 'b'), "\n";
// b
echo minWindow('bbaac', 'aba'), "\n";
// baa
echo minWindow('cabwefgewcwaefgcf', 'cae'), "\n";
// cwae

待优化点

如果 s = {\rm XX \cdots XABCXXXX}s=XX⋯XABCXXXX,t = {\rm ABC}t=ABC,那么显然 {\rm [XX \cdots XABC]}[XX⋯XABC] 是第一个得到的「可行」区间,得到这个可行区间后,我们按照「收缩」窗口的原则更新左边界,得到最小区间。
我们其实做了一些无用的操作,就是更新右边界的时候「延伸」进了很多无用的 \rm XX,更新左边界的时候「收缩」扔掉了这些无用的 \rm XX,做了这么多无用的操作,只是为了得到短短的 \rm ABCABC。
没错,其实在 ss 中,有的字符我们是不关心的,我们只关心 tt 中出现的字符,我们可不可以先预处理 ss,扔掉那些 tt 中没有出现的字符,然后再做滑动窗口呢?也许你会说,这样可能出现 \rm XXABXXCXXABXXC 的情况,在统计长度的时候可以扔掉前两个 \rm XX,但是不扔掉中间的 \rm XX,怎样解决这个问题呢?优化后的时空复杂度又是多少?

代码

function minWindow($s, $t)
{
    // $left、$right 窗口左右边界值
    // $start、$len 最小覆盖子串的开始位置及长度
    $left = $right = $start = 0;
    $len = PHP_INT_MAX;
    // $target、$window 目标子串的各字符出现次数、当前窗口的
    $target = $window = [];
    $tLen = strlen($t);
    for ($i = 0; $i < $tLen; $i++) {
        $target[$t[$i]] = isset($target[$t[$i]]) ? $target[$t[$i]] + 1 : 0;
    }
    // $match 当前窗口字符匹配次数
    $match = 0;

    $sLen = strlen($s);
    while ($right < $sLen) {
        $thisCharacter = $s[$right];
        if (isset($target[$thisCharacter])) {
            $window[$thisCharacter] = isset($window[$thisCharacter]) ? $window[$thisCharacter] + 1 : 0;
            if ($window[$thisCharacter] == $target[$thisCharacter]) {
                ++$match;
            }
        }
        ++$right;

        // 妙! $match == count($target) 说明此时目标字符都出现了1次或某些字符出现次数偏多了
        while ($match == count($target)) {
            if ($len > $right - $left) {
                $start = $left;
                $len = $right - $left;
            }

            $thisCharacter = $s[$left];
            if (isset($target[$thisCharacter])) {
                // 妙!
                // 预设窗口左边界值向右增大,减少当前窗口字符匹配次数:
                // 若当前目标字符过多则会反复循环去除
                // 若当前目标字符出现次数合适则会退出循环
                --$window[$thisCharacter];
                if ($window[$thisCharacter] < $target[$thisCharacter]) {
                    --$match;
                }
            }
            ++$left;
        }
    }

    return $len == PHP_INT_MAX ? '' : substr($s, $start, $len);
}

参考

https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-php-by-salmonl-2/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值