常规解法
先扩大窗口:从左往右找目标字符并记录出现次数。
再缩小窗口:每找到一次目标字符即尝试缩小,看目标字符是否都出现了一次,且记录的字符出现次数大于目标字符应出现次数。
代码
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);
}