【问题标题】:Replace wildcards in a binary string avoiding three identical consecutive letters替换二进制字符串中的通配符,避免三个相同的连续字母
【发布时间】:2021-11-17 18:48:21
【问题描述】:

给定一个长度为 N 的字符串 S,返回一个字符串,它是将字符串 S 中的每个 '?' 替换为 'a''b' 字符并且不包含三个相同的连续字母(在其他字,'aaa''bbb' 都不会出现在处理过的字符串中)。

例子:

Given S="a?bb", output= "aabb"

Given S="??abb", output= "ababb" or "bbabb" or "baabb"

Given S="a?b?aa", output= "aabbaa"

1

我使用回溯解决了这个问题,但我的解决方案很慢,并且不适用于更大的 N 值,有更好的方法吗?

【问题讨论】:

标签: c++ algorithm recursion dynamic-programming backtracking


【解决方案1】:

我很快试用了我在 cmets 中提出的解决方案:

#include <iostream>
#include <string>

int main(void)
{
    std::string s = "?????aaa??bbaa????";
    std::cout << s <<'\n';
    for(char &c:s)
        if(c=='?')
            c='a';
    
    std::cout << s <<'\n';

    for(int i =0; i<s.size()-2; i++)
    {
        if(s[i] == 'a' 
        && s[i+1] == 'a' 
        && s[i+2] == 'a' )
        {
            s[i+1] = 'b';
            i++;
        }
    }
    std::cout << s <<'\n';
    return 0;
}

它似乎符合给定的规则。 但也许有一些情况需要检查和修复。

【讨论】:

  • 很确定这不符合要求 - 您的程序可能会修改原始字符串中的非? 字符。例如,如果输入是"?aa",那么输出应该"baa",但你的程序会输出"aba"
  • 我想我的想法是替换所有的'?'最初带有“a”的值并记住“?”的位置.之后,您只检查“?”位置并满足给定的约束。
  • @Federico 我认为这是正确的(经过一些优化),因为我无法反驳它。我会对此进行更多研究。谢谢
  • 我实现了上面的方法,但它不起作用。考虑 S="a?b?aa"。你替换所有的'?使用 a 并记住索引 0 和 2(“?”)。这个想法是在找到 s[i]==s[i+1] && s[i]==s[i-1] 之后用 b 替换 a。但是,它不起作用,因为最后 3 个字符变成了“aaa”。即使这次你对所有字符进行第二遍并更改不遵循约束的字符,那么你可能会弄乱之前的字符。
【解决方案2】:

(另请参阅我的贪婪O(n) time, O(1) space solution,其中包括随机测试,与 MTO 的代码进行比较。)

O(n) 动态程序有四种状态:一个字符是a_firsta_secondb_firstb_second。让dp[i] 表示创建一个有效字符串直到索引i 的可能性。那么:

dp[i][a_first] = True
dp[i][a_second] = True
dp[i][b_first] = True
dp[i][b_second] = True
  where i < 0

if s[i] == '?':
  dp[i][a_first] = dp[i-1][b_first] or dp[i-1][b_second]
  dp[i][a_second] = dp[i-1][a_first]
  dp[i][b_first] = dp[i-1][a_first] or dp[i-1][a_second]
  dp[i][b_second] = dp[i-1][b_first]
  
else if s[i] == 'a':
  dp[i][a_first] = dp[i-1][b_first] or dp[i-1][b_second]
  dp[i][a_second] = dp[i-1][a_first]
  dp[i][b_first] = False
  dp[i][b_second] = False
  
else:
  dp[i][a_first] = False
  dp[i][a_second] = False
  dp[i][b_first] = dp[i-1][a_first] or dp[i-1][a_second]
  dp[i][b_second] = dp[i-1][b_first]

where i ≥ 0

要获取字符串,我们可以沿着始终保持 True 并保持连续字符约束的向后路径。

Python 代码:

def f(s):
  dp = [[None] * 4 for _ in range(len(s))]

  def get_dp(i, j):
    return True if i < 0 else dp[i][j]

  for i in range(len(s)):
    if s[i] == '?':
      dp[i][0] = get_dp(i-1, 2) or get_dp(i-1, 3)
      dp[i][1] = get_dp(i-1, 0) 
      dp[i][2] = get_dp(i-1, 0) or get_dp(i-1, 1)
      dp[i][3] = get_dp(i-1, 2)
  
    elif s[i] == 'a':
      dp[i][0] = get_dp(i-1, 2) or get_dp(i-1, 3)
      dp[i][1] = get_dp(i-1, 0)
      dp[i][2] = False
      dp[i][3] = False
  
    else:
      dp[i][0] = False
      dp[i][1] = False
      dp[i][2] = get_dp(i-1, 0) or get_dp(i-1, 1)
      dp[i][3] = get_dp(i-1, 2)

  # Build the output
  result = []

  i = len(s) - 1
  need = None

  while i >= 0:
    if dp[i][0] and need != 'b':
      result.append('a')
      need = None if (len(result) == 1 or result[-2] == 'b') else 'b'
      i-= 1
    elif dp[i][1] and need != 'b':
      result.extend(['a', 'a'])
      need = 'b'
      i -= 2
    elif dp[i][2] and need != 'a':
      result.append('b')
      need = None if (len(result) == 1 or result[-2] == 'a') else 'a'
      i -= 1
    elif dp[i][3] and need != 'a':
      result.extend(['b', 'b'])
      need = 'a'
      i -= 2
    else:
      return ""

  return "".join(reversed(result))

输出:

strs = [
  "a?bb", # "aabb"
  "??abb", # "ababb" or "bbabb" or "baabb"
  "a?b?aa", # "aabbaa"
  "?bb?",
  "aa?bb", # NO SOLUTION
  "aa??aa", # "aabbaa"
  "ab???bb?",
  "????",
  "??",
  "?????",
  "??????"
]

for s in strs:
  print("%s\n%s\n" % (s, f(s)))

"""
a?bb
aabb

??abb
baabb

a?b?aa
aabbaa

?bb?
abba

aa?bb


aa??aa
aabbaa

ab???bb?
abbaabba

????
abaa

??
aa

?????
aabaa

??????
baabaa
"""

【讨论】:

  • 我对动态编程不是很满意。我还在努力学习它。但是您的代码似乎有效。如果所有字符都是“?”,您的代码是否有效
  • @infernus-85 是的。刚刚添加了四个示例。
  • dp[i][0]、dp[i][1]、dp[i][2]、dp[i][3]这四种dp状态分别代表什么?
  • @infernus-85 dp的四种状态dp[i][0], dp[i][1], dp[i][2], dp[i][3] 代表是否不是结果的索引i 处的字符可以是“第一个a”、“第二个a”、“第一个b”或“第二个b”。例如,如果前一个字符可以是“第一个 a”,则一个字符只能是“第二个 a”——尝试遵循类似的常识逻辑来理解其他状态的条件。
【解决方案3】:

一种迭代回溯解决方案。

  • isValid 仅检查在位置pos 插入字符c 是否会产生问题,而不遍历整个字符串;

  • 维护一个变量int dir = +-1 以了解我们在字符串中是向前还是向后移动(这很重要,以便我们在跳过原始字符串中的非? 字符时知道向哪个方向移动);

  • if/else if/else 的森林来决定我们在哪里(非? 跳过,或者新鲜的? 继续前进,或者已经尝试过a,或者已经尝试过ab );

  • solve 有一个布尔返回值,如果找到解决方案,则返回 true (pos == s.length()),如果未找到解决方案,则返回 false (pos == (unsigned int) -1)。

#include <iostream>
#include <vector>

bool isValid(char c, std::string const &s, unsigned int pos)
{
    return ((pos < 2 || s[pos - 2] != c || s[pos - 1] != c)
         && (pos < 1 || pos + 1 >= s.length() || s[pos - 1] != c || s[pos + 1] != c)
         && (pos + 2 >= s.length() || s[pos + 1] != c || s[pos + 2] != c));
}
bool solve(std::string const &in, std::string &out)
{
    unsigned int pos = 0;
    int dir = 1;
    out = in;
    while (pos < in.size())
    {
        if (in[pos] != '?')  // nothing to do: keep moving (forward or backward)
        {
            pos += dir;
        }
        else if (out[pos] == '?')  // going forward, will try 'a' then 'b'
        {
            dir = 1;
            if (isValid('a', out, pos))
            {
                out[pos] = 'a';
                pos += dir;
            }
            else if (isValid('b', out, pos))
            {
                out[pos] = 'b';
                pos += dir;
            }
            else
            {
                dir = -1;
                pos += dir;
            }
        }
        else if (out[pos] == 'a' && isValid('b', out, pos)) // going backward, already tried 'a'
        {
            out[pos] = 'b';
            dir = 1;
            pos += dir;
        }
        else // already tried everything: backtrack
        {
            dir = -1;
            out[pos] = '?';
            pos += dir;
        }
    }
    return (pos == in.size());
}

int main(void)
{
  std::vector<std::string> ins  = {"a?bb", "??abb", "a?b?aa", "aa?bb"};
  std::vector<std::string> outs = {"", "", "", "", ""};
  for (unsigned int i = 0; i < ins.size(); ++i)
  {
      if (solve(ins[i], outs[i]))
          std::cout << ins[i] << "  -->  " << outs[i] << std::endl;
      else
          std::cout << ins[i] << "  -->  NO SOLUTION" << std::endl;
  }
  return 0;
}

输出:

a?bb  -->  aabb
??abb  -->  ababb
a?b?aa  -->  aabbaa
aa?bb  -->  NO SOLUTION

【讨论】:

  • 谢谢。这是回溯解决方案的优化版本。我从中学到了很多。
【解决方案4】:

首先x??y 有超过 1 个?总会过去的

  • a??b =&gt; abab(两侧长度不增加,*无影响*跟随)
  • a??a =&gt; abba(无影响)
  • a???????????????????b =&gt; ab?????????????????ab

所以你只需要考虑单个?的情况

  • aa? =&gt; aab(没有其他可能)
  • a?a =&gt; aba(无影响)

所以唯一的问题是a?b

  • aa?b =&gt; aabb(如aa? 中所述)
  • ba?b =&gt; baab(无影响)
  • ^a?b => ^aab(仅用于启动,无影响)

您最多进行了 2 次回顾和 2 次展望,所以这是一个 O(n) 解决方案,

如果它可以包含无效输入,只需运行另一个O(n) 检查

我在另一个答案中添加了possible implementation

【讨论】:

【解决方案5】:

在 JavaScript 中实现@appleapple's O(N) solution(但移植到 C++ 应该相对简单):

let solve = (str) => {
  let opposite = {"a":"b","b":"a"};
  let curr_idx = 0;
  let output = [];
  
  let lookahead  = (x) => ((curr_idx+x<0)||(curr_idx+x>=str.length)?null:str[curr_idx + x]);
  let lookbehind = (x) => (curr_idx-x<0?null:output[curr_idx - x])
  let matches    = (x,y) => (x == y || x == null || y == null);
  let is_replacement = (x) => (x == '?');
  
  while ( curr_idx < str.length )
  {
    let curr = lookbehind(1) || 'b';
    let i = 0;
    let next = lookahead(i);
    while (is_replacement(next))
    {
      ++i;
      next = lookahead(i);
    }
    
    if (next == null)
    {
      // Found the end of the string.
      // Append characters opposite to the previous for each ?
      for (; i > 0; --i)
      {
        curr = opposite[curr];
        output.push( curr );
      }
      break;
    }

    if (i > 1)
    {
      // Found multiple successive ?s
      // Handle the first half of the ?s by
      // Append alternating characters from the previous character.
      let j = 0;
      for (; j < i/ 2; ++j)
      {
        curr = opposite[curr];
        output.push( curr );
      }
      // Then handle the second half of the ?s
      // append alternating characters to the next character after the ?s.
      for (; j < i; ++j)
      {
        output.push( (j%2) == (i%2) ? next : opposite[next] );
      }
    }
    else if (i == 1)
    {
      // Found a single ?
      let prev = lookbehind(2);
      if (curr == prev && curr == opposite[next] && next == lookahead(2))
      {
        // No solution.
        return null;
      }
      if (curr == prev || matches(curr, next))
      {
        output.push( opposite[curr] );
      }
      else
      {
        output.push( curr );
      }
    }

    if ( next != null )
    {
      // Output the next non-? character.
      output.push( next );
    }
    curr_idx += i + 1;
  }
  
  return output.join("");
};

let tests = [
  "?",
  "a?",
  "a??",
  "a???",
  "b?",
  "b??",
  "b???",
  "a?a",
  "a?b",
  "b?a",
  "b?b",
  "a??a",
  "a??b",
  "b??a",
  "b??b",
  "aa?",
  "ba?",
  "a?bb",
  "?bb?",
  "??abb",
  "a?b?aa",
  "ab???bb?",
  "aa?bb",
];

for ( let test of tests )
{
  console.log( `${test} -> ${solve(test)}` );
}

更新 - 所有可能的解决方案

可以将问题减少到 10 个规则从左到右在字符串上运行,查看最多 6 个字符的窗口(2 个后视和 4 个前瞻),这样规则可以生成所有可能的字符串的解决方案:

Rules:
0: ss?dd_ -> No solution
1: __??ss -> __?dss
2: _s?s__ -> _sds__
3: ss?___ -> ssd___
4: __?ss_ -> __dss_
5: DS?DS_ -> DSsDS_ | DSdDS_
6: DS??sD -> DSsdsD | DSddsD | DSdssD
7: Ds??dS -> DsdsdS | DssddS
8:  ^??X_ ->  ^s?X_ |  ^d?X_
9: Ds??X_ -> DssdX_ | Dsd?X_

Notations:
 s: Same character.
 d: The opposite of the same character.
 S: Either the same character or the end-of-the-string.
 D: Either the opposite of the same character or the start- or end-of-the-string.
 _: Any character or the start- or end-of-the-string.
 ?: A character to replace.
 X: Either a character to replace or the end-of-the-string.
 ^: The start-of-the-string.

(注意:规则 1 与规则 4 基本相同,但先处理后面的替换字符。)

这导致了 JavaScript 代码:

function* solve(
  str,
  i = 0,
  depth = 0
)
{
  let data        = Array.from(str);
  let chr         = (n) => (n < 0 || n > data.length ? null : data[n]);

  let lookbehind2 = chr(i - 2);
  let lookbehind1 = chr(i - 1);
  let current     = chr(i);
  let lookahead1  = chr(i + 1);
  let lookahead2  = chr(i + 2);
  let lookahead3  = chr(i + 3);

  const DEBUG     = (rule) => {
    if (false)
    {
      console.log(`${"".padStart(depth)}Rule ${rule}@${i}: ${str} -> ${data.join("")}`)
    }
  };

  let stepForward = (steps) => {
    for (let n = 0; n < steps; ++n)
    {
      ++i;
      lookbehind2 = lookbehind1;
      lookbehind1 = current;
      current     = lookahead1;
      lookahead1  = lookahead2;
      lookahead2  = lookahead3;
      lookahead3  = chr(i + 3);
    }
  }

  let isSolved = (ch) => (ch == 'a' || ch == 'b');
  let isSame = (ch1, ch2) => (isSolved(ch1) && ch1 == ch2);
  let opposite = (ch) => ({"a":"b","b":"a"}[ch]);
  let isOpposite = (ch1, ch2) => (isSolved(ch1) && ch1 == opposite(ch2));
  let isEndOfString = (ch) => (ch == null);
  let isStartOfString = (ch) => (ch == null);

  while( !isEndOfString(current) )
  {
    if (current != '?')
    {
      stepForward(1);
      continue;
    }
    
    // Rules:
    //  0: ss?dd_ -> No solution
    //  1: __??ss -> __?dss
    //  2: _s?s__ -> _sds__
    //  3: ss?___ -> ssd___
    //  4: __?ss_ -> __dss_
    //  5: DS?DS_ -> DSsDS_ | DSdDS_
    //  6: DS??sD -> DSsdsD | DSddsD | DSdssD
    //  7: Ds??dS -> DsdsdS | DssddS
    //  8:  $??X_ ->  $s?X_ |  $d?X_
    //  9: Ds??X_ -> DssdX_ | Dsd?X_
    //
    // Notations:
    //   s: Same character.
    //   d: The opposite of the same character.
    //   S: Either the same character or the end-of-the-string.
    //   D: Either the opposite of the same character or the start- or end-of-the-string.
    //   _: Any character or the start- or end-of-the-string.
    //   ?: A character to replace.
    //   X: Either a character to replace or the end-of-the-string.
    //   $: The end-of-the-string.
    // -----------------------------------------------------------
    
    // Rule 0: ss?dd_ -> No solution
    if (
      isSame(lookbehind2, lookbehind1)
      && isSame(lookahead1, lookahead2)
      && lookbehind1 != lookahead1
    )
    {
      DEBUG(0);
      return;
    }

    // Rule 1: __??ss -> __?dss
    if (lookahead1 == '?' && isSame(lookahead2, lookahead3))
    {
      data[i + 1] = lookahead1 = opposite(lookahead2);
      DEBUG(1);
    }
      
    // Rule 2: _s?s__ -> _sds__
    if (isSame(lookbehind1, lookahead1))
    {
      data[i] = current = opposite(lookahead1);
      DEBUG(2);
      stepForward(2);
      continue;
    }

    // Rule 3: ss?___ -> ssd___
    if (isSame(lookbehind2, lookbehind1))
    {
      data[i] = current = opposite(lookbehind1);
      DEBUG(3);
      stepForward(1);
      continue;
    }

    // Rule 4: __?ss_ -> __dss_
    if (isSame(lookahead1, lookahead2))
    {
      data[i] = current = opposite(lookahead1);
      DEBUG(4);
      stepForward(3);
      continue;
    }

    // Rule 5: DS?DS_ -> DSsDS_ | DSdDS_
    if (lookahead1 != '?')
    {
      data[i] = current = "a";
      DEBUG(5);
      for (solution of solve(data.join(""), i + 2, depth + 1))
      {
        yield solution;
      }
      data[i] = current = "b";
      DEBUG(5);
      for (solution of solve(data.join(""), i + 2, depth + 1))
      {
        yield solution;
      }
      return;
    }

    // Rule 6: DS??sD -> DSsdsD | DSddsD | DSdssD
    if (
      isSame(lookbehind1, lookahead2)
      || (isStartOfString(lookbehind1) && isSolved(lookahead2))
    )
    {
      data[i]   = current    = lookahead2;
      data[i+1] = lookahead1 = opposite(lookahead2);
      DEBUG(6);
      for (solution of solve(data.join(""), i + 3, depth + 1))
      {
        yield solution;
      }
      data[i]   = current    = opposite(lookahead2);
      data[i+1] = lookahead1 = opposite(lookahead2);
      DEBUG(6);
      for (solution of solve(data.join(""), i + 3, depth + 1))
      {
        yield solution;
      }
      data[i]   = current    = opposite(lookahead2);
      data[i+1] = lookahead1 = lookahead2;
      DEBUG(6);
      for (solution of solve(data.join(""), i + 3, depth + 1))
      {
        yield solution;
      }
      return;      
    }
    
    
    // Rule 7: Ds??dS -> DsdsdS | DssddS
    if (isOpposite(lookahead2, lookbehind1))
    {
      data[i]   = current    = lookahead2;
      data[i+1] = lookahead1 = opposite(lookahead2);
      DEBUG(7);
      for (solution of solve(data.join(""), i + 3, depth + 1))
      {
        yield solution;
      }
      data[i]   = current    = opposite(lookahead2);
      data[i+1] = lookahead1 = lookahead2;
      DEBUG(7);
      for (solution of solve(data.join(""), i + 3, depth + 1))
      {
        yield solution;
      }
      return;      
    }

    // Rule 8:  $??X_ ->  $s?X_ |  $d?X_
    // Rule 9: Ds??X_ -> DssdX_ | Dsd?X_
    if (lookahead2 == "?" || isEndOfString(lookahead2))
    {
      if (isStartOfString(lookbehind1))
      {
        data[i] = current = "a";
        DEBUG(8);
        for (solution of solve(data.join(""), i + 1, depth + 1))
        {
          yield solution;
        }
        data[i] = current = "b";
        DEBUG(8);
        for (solution of solve(data.join(""), i + 1, depth + 1))
        {
          yield solution;
        }
      }
      else
      {
        data[i] = current = opposite(lookbehind1);
        DEBUG(9);
        for (solution of solve(data.join(""), i + 1, depth + 1))
        {
          yield solution;
        }
        data[i]   = current    = lookbehind1;
        data[i+1] = lookahead1 = opposite(lookbehind1);
        DEBUG(9);
        for (solution of solve(data.join(""), i + 2, depth + 1))
        {
          yield solution;
        }
      }
      return;
    }
    
    throw `Invalid state ${data.join("")}`;
  }
  
  yield data.join("");       
}        

let tests = [
  "?",
  "??",
  "???",
  "a?",
  "a??",
  "a???",
  "b?",
  "b??",
  "b???",
  "a?a",
  "a?b",
  "b?a",
  "b?b",
  "a??a",
  "a??b",
  "b??a",
  "b??b",
  "aa?",
  "ba?",
  "a?bb",
  "?bb?",
  "?a",
  "?aa",
  "??aa",
  "???aa",
  "????aa",
  "?ab",
  "??ab",
  "???ab",
  "????ab",
  "a?b?aa",
  "ab???bb?",
  "aa?bb",
  "a?b?a?bb",
  "?a?b?aa",
];

for ( let test of tests )
{
  console.log( `${test} -> ${[...solve(test)]}` );
}

如果您想要一个单一的解决方案,那么您应该能够只为每个规则获取第一个匹配项,并在 O(N) 时间和 O(1) 内存求解器中实现它。


更新 2:C++ 解决方案

与 JavaScript 解决方案的算法相同,但使用一堆部分解决方案(而不是递归调用生成器函数的 JavaScript 解决方案)。

#include <iostream>
#include <list>

class PartialSolution
{
  public:
    const std::string partial_solution;
    const int position;

    PartialSolution(
      std::string const &solution,
      const int pos
    ):
      partial_solution(solution),
      position(pos)
    {}
};

class Solver
{
    const bool DEBUGGING = false;
    std::string str;
    int position;
    
    char lookbehind2;
    char lookbehind1;
    char current;
    char lookahead1;
    char lookahead2;
    char lookahead3;
    
    char get_character(int pos) const
    {
      if ( pos < 0 || pos >= str.length() )
      {
        return '\0';
      }
      return str[pos];
    }
    
    void load_partial_solution(PartialSolution const &ps)
    {
      str = ps.partial_solution;
      position = ps.position;
      
      lookbehind2 = get_character(position - 2);
      lookbehind1 = get_character(position - 1);
      current     = get_character(position + 0);
      lookahead1  = get_character(position + 1);
      lookahead2  = get_character(position + 2);
      lookahead3  = get_character(position + 3);

      if (DEBUGGING)
      {
        std::cout << "Load @" << position << ": " << str << "\n";
      }
    }

    void step_forward(int n)
    {
      for (int i = 0; i < n; ++i)
      {
        ++position;
        lookbehind2 = lookbehind1;
        lookbehind1 = current;
        current     = lookahead1;
        lookahead1  = lookahead2;
        lookahead2  = lookahead3;
        lookahead3  = get_character(position + 3);
      }
    }
    
    bool is_solved(char ch) const
    {
      return ch == 'a' || ch == 'b';
    }

    bool is_same(char ch1, char ch2) const
    {
      return is_solved(ch1) && ch1 == ch2;
    }

    char opposite(char ch) const
    {
      switch(ch)
      {
        case 'a': return 'b';
        case 'b': return 'a';
        default:
             return '\0';
      }
    }
    
    bool is_opposite(char ch1, char ch2) const
    {
      return is_solved(ch1) && ch1 == opposite(ch2);
    }

    bool is_end_of_string(char ch) const
    {
      return ch == '\0';
    }

    bool is_start_of_string(char ch) const
    {
      return ch == '\0';
    }
    
    void DEBUG(int rule, bool pushback = false) const
    {
      if (DEBUGGING)
      {
        std::cout << (pushback?"Save: ":"") << "Rule " << rule << "@" << position << ": " << str << "\n";
      }
    }
  public:
    std::list<std::string> solve(std::string const&);
};


std::list<std::string> Solver::solve(std::string const& data)
{
  std::list<PartialSolution> partial_solutions = { PartialSolution(data, 0) };
  std::list<std::string> solutions = {};
  
  while( !partial_solutions.empty() )
  {
    load_partial_solution( partial_solutions.back() );
    partial_solutions.pop_back();
    
    while( !is_end_of_string(current) )
    {
      if (current != '?')
      {
        step_forward(1);
        continue;
      }

      // Rules:
      //  0: ss?dd_ -> No solution
      //  1: __??ss -> __?dss
      //  2: _s?s__ -> _sds__
      //  3: ss?___ -> ssd___
      //  4: __?ss_ -> __dss_
      //  5: DS?DS_ -> DSsDS_ | DSdDS_
      //  6: DS??sD -> DSsdsD | DSddsD | DSdssD
      //  7: Ds??dS -> DsdsdS | DssddS
      //  8:  $??X_ ->  $s?X_ |  $d?X_
      //  9: Ds??X_ -> DssdX_ | Dsd?X_
      //
      // Notations:
      //   s: Same character.
      //   d: The opposite of the same character.
      //   S: Either the same character or the end-of-the-string.
      //   D: Either the opposite of the same character or the start- or end-of-the-string.
      //   _: Any character or the start- or end-of-the-string.
      //   ?: A character to replace.
      //   X: Either a character to replace or the end-of-the-string.
      //   $: The end-of-the-string.
      // -----------------------------------------------------------

      // Rule 0: ss?dd_ -> No solution
      if (
        is_same(lookbehind2, lookbehind1)
        && is_same(lookahead1, lookahead2)
        && lookbehind1 != lookahead1
      )
      {
        DEBUG(0);
        goto no_solution;
      }

      // Rule 1: __??ss -> __?dss
      if (lookahead1 == '?' && is_same(lookahead2, lookahead3))
      {
        lookahead1 = opposite(lookahead2);
        str[position+1] = lookahead1;
        DEBUG(1);
      }
      
      // Rule 2: _s?s__ -> _sds__
      if (is_same(lookbehind1, lookahead1))
      {
        str[position] = current = opposite(lookahead1);
        DEBUG(2);
        step_forward(2);
        continue;
      }

      // Rule 3: ss?___ -> ssd___
      if (is_same(lookbehind2, lookbehind1))
      {
        str[position] = current = opposite(lookbehind1);
        DEBUG(3);
        step_forward(1);
        continue;
      }

      // Rule 4: __?ss_ -> __dss_
      if (is_same(lookahead1, lookahead2))
      {
        str[position] = current = opposite(lookahead1);
        DEBUG(4);
        step_forward(3);
        continue;
      }

      // Rule 5: DS?DS_ -> DSsDS_ | DSdDS_
      if (lookahead1 != '?')
      {
        str[position] = current = 'b';
        DEBUG(5, true);
        partial_solutions.emplace_back(str, position + 2);
        str[position] = current = 'a';
        DEBUG(5);
        step_forward(2);
        continue;
      }

      // Rule 6: DS??sD -> DSsdsD | DSddsD | DSdssD
      if (
        is_same(lookbehind1, lookahead2)
        || (is_start_of_string(lookbehind1) && is_solved(lookahead2))
      )
      {
        str[position]   = current    = opposite(lookahead2);
        str[position+1] = lookahead1 = lookahead2;
        DEBUG(6, true);
        partial_solutions.emplace_back(str, position + 3);
        str[position]   = current    = opposite(lookahead2);
        str[position+1] = lookahead1 = opposite(lookahead2);
        DEBUG(6, true);
        partial_solutions.emplace_back(str, position + 3);
        str[position]   = current    = lookahead2;
        str[position+1] = lookahead1 = opposite(lookahead2);
        DEBUG(6);
        step_forward(3);
        continue;
      }

      // Rule 7: Ds??dS -> DsdsdS | DssddS
      if (is_opposite(lookahead2, lookbehind1))
      {
        str[position]   = current    = opposite(lookahead2);
        str[position+1] = lookahead1 = lookahead2;
        DEBUG(7, true);
        partial_solutions.emplace_back(str, position + 3);
        str[position]   = current    = lookahead2;
        str[position+1] = lookahead1 = opposite(lookahead2);
        DEBUG(7);
        step_forward(3);
        continue;
      }

      // Rule 8:  $??X_ ->  $s?X_ |  $d?X_
      // Rule 9: Ds??X_ -> DssdX_ | Dsd?X_
      if (lookahead2 == '?' || is_end_of_string(lookahead2))
      {
        if (is_start_of_string(lookbehind1))
        {
          str[position] =  current = 'b';
          DEBUG(8, true);
          partial_solutions.emplace_back(str, position + 1);
          str[position] = current = 'a';
          DEBUG(8);
          step_forward(1);
          continue;
        }
        else
        {
          str[position]   = current    = lookbehind1;
          str[position+1] = lookahead1 = opposite(lookbehind1);
          DEBUG(9, true);
          partial_solutions.emplace_back(str, position + 2);
          str[position]   = current    = opposite(lookbehind1);
          str[position+1] = lookahead1 = '?';
          DEBUG(9);
          continue;
        }
      }

      throw std::invalid_argument(str);
    }
    
    solutions.push_back(str);
    
    no_solution:;
  }
  
  return solutions;
}

void run_test(
  Solver &solver,
  std::string const &test
)
{
  std::cout << test << " -> ";
  std::list<std::string> solutions = solver.solve(test);
  int size = solutions.size();
  for (const auto& solution : solutions)
  {
    std::cout << solution;
    if (--size != 0)
    {
      std::cout << ", ";
    }
  }
  std::cout << "\n";
}

int main()
{
  std::list<std::string> tests = {
    "?",
    "??",
    "???",
    "a?",
    "a??",
    "a???",
    "b?",
    "b??",
    "b???",
    "a?a",
    "a?b",
    "b?a",
    "b?b",
    "a??a",
    "a??b",
    "b??a",
    "b??b",
    "aa?",
    "ba?",
    "a?bb",
    "?bb?",
    "?a",
    "?aa",
    "??aa",
    "???aa",
    "????aa",
    "?ab",
    "??ab",
    "???ab",
    "????ab",
    "a?b?aa",
    "ab???bb?",
    "aa?bb",
    "a?b?a?bb",
    "?a?b?aa",
  };
  Solver solver = Solver();
  for (const auto& test : tests)
  {
    run_test(solver, test);
  }
}

【讨论】:

  • 谢谢,现在我对算法的理解更好了。前瞻和后瞻功能在做什么?据我了解,这个算法是基于滑动窗口的。向前看和向后看是找到两个不是通配符的字符,并且在通配符之间。我说的对吗?
  • @infernus-85 和 MTO,我相信我在贪婪的 O(n) 时间、O(1) 空间解决方案中修复了一个错误。我在最后包含了测试 1000 个随机(但有效)输入的代码。这和我的贪婪方法似乎都过去了。 stackoverflow.com/a/69327013/2034787 在这里:ideone.com/N9vXFa
  • @infernus-85 Look-behind 查看字符串中与当前正在求解的位置的相对偏移量的先前字符。 Look-ahead 查看字符串中在当前位置被解决后与第一个非替换字符相对偏移的字符。
  • @infernus-85 和 MT0,apple 实现了他们的解决方案。这是天才——太短了!见here(这里还有一个带有随机测试的python版本ideone.com/oGwDef
  • @גלעדברקן 我查看了 c++ 代码,现在我终于理解了算法。我遇到的唯一问题是字符串的开头。一旦我们处理了所有的起始排列,那么从那里开始就很容易了。
【解决方案6】:

下面的贪婪版本似乎有效。这样,除了一个输出字符串之外,我们还可以拥有恒定的空间。有人可以帮忙找到一个反例吗?我在实现简单的动态程序之前写了这个想法(参见this revision)。尽量保持两个相同字符的序列。

下面代码的最后一部分包括使用 this 和 MTO's code 进行随机测试。

Python 代码:

def is_valid(s):
  if not s:
    return True
  char = "x"
  count = 0
  for c in s:
    if c != char:
      char, count = c, 1
    else:
      count += 1
      if count == 3:
        return False
  return True


# My greedy solution
def f(s):
  if len(s) == 2:
    return s.replace('?', 'a')
    
  aa = ['a', 'a']
  bb = ['b', 'b']

  out = []
  i = 0
  a, b = 0, 0

  while i < len(s):
    if s[i] == 'a' or (s[i] == '?' and b == 2):
      if s[i] == 'a' and a == 2:
        if s[i-3:i-1] == "??":
          out[i-3], out[i-2] = out[i-2], out[i-3]
          a -= 1
        else:
          return ""
      out.append('a')
      a, b = a + 1, 0
      i += 1
    elif s[i] == 'b' or (s[i] == '?' and a == 2):
      if s[i] == 'b' and b == 2:
        if s[i-3:i-1] == "??":
          out[i-3], out[i-2] = out[i-2], out[i-3]
          b -= 1
        else:
          return ""
      out.append('b')
      a, b = 0, b + 1
      i += 1
    elif i == len(s) - 1:
      out.append('a' if b else 'b')
      i += 1
    elif i == len(s) - 2:
      if s[i+1] == '?':
        out.extend(aa if b else bb)
      else:
        out.append('a' if b else 'b')
        out.append(s[i+1])
      i += 2
    elif s[i+1] == '?':
      if a:
        out.append('a')
        a, b = a + 1, 0
      else:
        out.append('b')
        a, b = 0, b + 1
      i += 1
    elif s[i+1] == 'a':
      if a or b < 2:
        out.append('b')
        a, b = 0, b + 1
      else:
        out.append('a')
        a, b = a + 1, 0
      i += 1
    elif s[i+1] == 'b':
      if b or a < 2:
        out.append('a')
        a, b = a + 1, 0
      else:
        out.append('b')
        a, b = 0, b + 1
      i += 1

  return ''.join(out)


# https://stackoverflow.com/a/69322213/2034787
# Code by MTO
def solve(_str):
  opposite = {"a": "b", "b": "a"}
  curr_idx = 0
  output = []
  
  lookahead  = lambda x: None if ((curr_idx + x < 0) or (curr_idx + x >= len(_str))) else _str[curr_idx + x]
  lookbehind = lambda x: None if curr_idx-x< 0 else output[curr_idx - x]
  matches = lambda x, y: x == y or x == None or y == None
  is_replacement = lambda x: x == '?'
  
  while curr_idx < len(_str):
    curr = lookbehind(1) or 'b'
    i = 0
    next = lookahead(i)
    while is_replacement(next):
      i += 1
      next = lookahead(i)
    
    if next == None:
      # Found the end of the string.
      # Append characters opposite to the previous for each ?
      for _ in range(i, 0, -1):
        curr = opposite[curr]
        output.append( curr )
      break

    if (i > 1):
      # Found multiple successive ?s
      # Handle the first half of the ?s by
      # Append alternating characters from the previous character.
      j = 0
      while j < i / 2:
        curr = opposite[curr]
        output.append( curr )
        j += 1
      # Then handle the second half of the ?s
      # append alternating characters to the next character after the ?s.
      while j < i:
        output.append( next if (j%2) == (i%2) else opposite[next] )
        j += 1
  
    elif i == 1:
      # Found a single ?
      prev = lookbehind(2)
      if curr == prev and curr == opposite[next] and next == lookahead(2):
        # No solution.
        return None

      if curr == prev or matches(curr, next):
        output.append( opposite[curr] )
      else:
        output.append( curr )

    if next != None:
      # Output the next non-? character.
      output.append( next )

    curr_idx += i + 1
  
  return ''.join(output)


strs = [
  "a?bb", # "aabb"
  "??abb", # "ababb" or "bbabb" or "baabb"
  "a?b?aa", # "aabbaa"
  "?bb?",
  "aa?bb", # NO SOLUTION
  "aa??aa", # "aabbaa"
  "ab???bb?",
  "????",
  "??",
  "?????",
  "??????",
  "?",
  "a?",
  "a??",
  "a???",
  "b?",
  "b??",
  "b???",
  "a?a",
  "a?b",
  "b?a",
  "b?b",
  "a??a",
  "a??b",
  "b??a",
  "b??b",
  "aa?",
  "ba?",
  "a?bb",
  "?bb?",
  "??abb",
  "a?b?aa",
  "ab???bb?",
  "aa?bb"
]


for s in strs:
  _solve = solve(s)
  _f = f(s)
  print("%s\n%s, %s, %s, %s\n" % (s, is_valid(_f), is_valid(_solve), _solve, _f))


import random

def get_random(n):
  char = 'x'
  count = 0
  out = []
  not_c = lambda c: 'a' if c == 'b' else 'b'

  for _ in range(n):
    c = ['a', 'b'][int(random.random() * 2)]
    if c != char:
      out.append(c)
      char, count = c, 1
    elif count == 2:
      out.append(not_c(c))
      char, count = not_c(c), 1
    else:
      out.append(c)
      count += 1

  for i in range(n):
     if int(random.random() * 2):
      out[i] = '?'

  return ''.join(out)

num_tests = 1000
n = 20

print("Starting random tests...")

for _ in range(num_tests):
  s = get_random(n)
  _solve = solve(s)
  _f = f(s)
  valid_solve = is_valid(_solve)
  valid_f = is_valid(_f)
  if not valid_f or not valid_solve:
    print(s, valid_f, valid_solve, _f, _solve)
    break

print("Done testing")

【讨论】:

    【解决方案7】:

    rules in my answer 的可能实现。


    这个实现是

    • 从左到右
    • 单次通过,2 次后视和 1 次前瞻(尽管进行了初始检查)
    • O(n) 时间复杂度
    • 可以是 O(1) 空间复杂度,因为上下文最多为 4 个字符
    • 检查无效输入

    先合并规则

    • a?? =&gt; ab?
      • a??b =&gt; abab (a??b =&gt; ab?b =&gt; abab)
      • a??a =&gt; abba
      • a???????????????????b =&gt; ab??????????????????b
    • ba?b =&gt; baab
    • ^a?b =&gt; ^aab
    • a? =&gt; ab 否则(也暗示a??
      • aa? =&gt; aab
      • a?a =&gt; aba
      • aa?b =&gt; aabb

    然后设置边界条件。

    规则需要字符串not?s开头(如果只是在另一个pass中填写则不需要)

    • ^?? =&gt; a?(就像我们在前面加上 b
    • ^?a =&gt; ba

    a?b 的情况下,该规则需要 2 回溯,所以我只是预先应用它以防止在主循环内进行检查

    • 预填^a?b =&gt; ^aab

    代码 (WandBox link)

    char inverse(char c){return c=='a'?'b':'a';}
    
    std::string solve(std::string s){
       
       /// boundary conditions
       
       if(s.size()<3){
          for(auto& c:s) if(c=='?') c='b';
          return s;
       }
       
       if(s.starts_with("??")) s[0]='a'; // ?? => a? // not really necessary if use another pass to fill prefix '?'s
       if(s.starts_with("?a")  || s.starts_with("?b"))  s[0]=inverse(s[1]); // ?a => ba // not really necessary as above
       if(s.starts_with("a??") || s.starts_with("b??")) s[1]=inverse(s[0]); // not really necessary, just to prevent access s[-1]
       if(s.starts_with("a?b") || s.starts_with("b?a")) s[1]=s[0]; // ^a?b => aab
       
       /// primary loop
       
       for(auto curr=s.data(); curr!=s.data()+s.size(); ++curr)
       if(*curr=='?'){
          if(curr[-1]!=curr[1] && curr[-2]==curr[1]) *curr=curr[-1]; // ba?b => baab
          else *curr = inverse(curr[-1]); // a? => b (rule coaslesing)
       }
        
       return s;
    }
    

    【讨论】:

      猜你喜欢
      • 2022-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-09
      相关资源
      最近更新 更多