替换二进制字符串中的通配符,避免三个相同的连续字母

Replace wildcards in a binary string avoiding three identical consecutive letters

给定一个长度为 N 的字符串 S,return 一个字符串,该字符串是将字符串 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<=500000

我使用回溯法解决了这个问题,但是我的解决方案很慢,并且对更大的 N 值不起作用,有没有更好的方法?

我很快尝试了我在评论中提出的解决方案:

#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;
}

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

(请参阅我的贪婪 ,其中包括随机测试,与 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
"""

迭代回溯解决方案。

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

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

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

  • solve 有一个布尔值 return 如果找到解决方案 (pos == s.length()),则该值为 true,或者 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

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

  • a??b => abab(两边不增加长度,*不影响*后面)
  • a??a => abba(无影响)
  • a???????????????????b => ab?????????????????ab

所以你只需要考虑单?

的情况
  • aa? => aab(没有其他可能性)
  • a?a => aba(无影响)

所以唯一的问题是a?b

  • aa?b => aabb(如 aa? 中所述)
  • ba?b => baab(无影响)
  • ^a?b => ^aab(仅起步,无影响)

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

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

我在另一个回答里加了一个)

在JavaScript中实现(但移植到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 条规则,从左到右对字符串进行操作,最多 window 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.

(注:规则一与规则四基本相同,只是先处理后面的替换字符。)

这导致 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 '[=13=]';
      }
      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 '[=13=]';
      }
    }
    
    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 == '[=13=]';
    }

    bool is_start_of_string(char ch) const
    {
      return ch == '[=13=]';
    }
    
    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);
  }
}

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

下面代码的最后一部分包括随机测试

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)


# 
# 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")

的可能实现。


这个实现是

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

先合并规则

  • a?? => ab?
    • a??b => abab (a??b => ab?b => abab)
    • a??a => abba
    • a???????????????????b => ab??????????????????b
  • ba?b => baab
  • ^a?b => ^aab
  • a? => ab 否则(也意味着 a??
    • aa? => aab
    • a?a => aba
    • aa?b => aabb

然后设置边界条件。

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

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

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

  • 预填 ^a?b => ^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;
}