Educational Codeforces Round 149 (Rated for Div. 2) C. Best Binary String
给一个字符串 \(s\) 包含 \(0, 1, ?\) 。
定义一个 \(01\) 串 \(s\) 的 \(cost\) 为:选择 \(s\) 的任意一个子段 \([l, r]\) 并 \(reverse\) 。将 \(s\) 变为一个非降序序列时的 \(reverse\) 最小次数即 \(cost\) 。
你可以让 \(s\) 的 \(?\) 换成 \(0/1\) ,使新 \(s\) 的 \(cost\) 最小。输出 \(cost\) 最小的 \(s\) 。
首先分析一个 \(01\) 串 \(s\) 的 \(cost\) ,即需要 \(reverse\) 多少次。
不难和证明发现使 \(s\) 非降序需要 \(reverse\) 的次数等于 \(s\) 中 \(10\) 子串的个数。
一:
可以使用 \(navie\) 的 \(dp\) 做法,\(dp_{i, j}\) 为考虑到前 \(i\) 个位置,以 \(j\) 结尾,可以产生的 \(10\) 串数量。
\(dp_{i, j}\) 做法为:初始所有状态为 \(inf\) 。假设考虑到第 \(i\) 位,则 \(1 \sim i - 1\) 的所有状态都已确定,即第 \(i\) 位可由前 \(i - 1\) 位递推。只需要处理第 \(1\) 为的边界状态即可。
-
需要求最小值,所以未知态初始化为 \(+\infty\) 。
-
边界处理
- 若 \(s_1 = '0'\ or\ s_1 = '1'\) ,\(dp_{1, s_i - '0'} = 0\) ,合法态唯一。
- 若 \(s_1 = ?\) ,\(dp_{1, 0} = dp_{1, 1} = 0\) ,合法态全部可能。
-
状态转移
- 若 \(s_i = '0'\ or\ s_i = '1'\) ,\(dp_{i, s_i - '0'} = \mathop{min}\limits_{k = 0,1}\ dp_{i - 1, k} + [k == 1\ and\ s_i == '0']\)
若 \(s_i = '?'\) ,\(dp_{i - 1, j} = \mathop{min}\limits_{k = 0, 1}\ dp_{i - 1, k} + [k == 0\ and\ j == 1]\) 。
- 若 \(s_i = '0'\ or\ s_i = '1'\) ,\(dp_{i, s_i - '0'} = \mathop{min}\limits_{k = 0,1}\ dp_{i - 1, k} + [k == 1\ and\ s_i == '0']\)
虽然上第 \(i\) 维可以被滚动掉,但是当 \(dp\) 需要使用 \(pre\) 记录路径,则需要保留该维。
显然使用辅助数组 \(pre\) 记录 \(dp\) 路径。只需在递推 \(dp_{i, j} = \mathop{min}\limits_{i = 0, 1}\ dp_{i - 1, k} + [k == 1\ and\ j == 0]\) 时记录同时维护最小值 \(mi\) 和 \(p = k\) ,则 \(pre_{i, j} = k\) 。
复原路径时:找到 \(dp_{n, j}\) 最小的 \(p = j\) ,然后 \(for\ n \sim 1\) ,\(p = dp_{i, p}\) 。前 \(n\) 个出现的 \(p\) 的逆序即路径。
view1
#include <bits/stdc++.h>
typedef long long ll;
void solve(){
std::string s; std::cin >> s; int n = s.length(); s = " " + s;
std::vector<std::array<int, 2> > dp(n + 1, {1 << 30, 1 << 30}), pre(n + 1);
if (s[1] == '1' || s[1] == '0') dp[1][s[1] - '0'] = 0;
else if (s[1] == '?') dp[1][0] = dp[1][1] = 0;
for (int i = 2; i <= n; i++) {
if (s[i] == '1' || s[i] == '0') {
int p = -1, mi = 1 << 30;
for (int k = 0; k <= 1; k++) {
if ( dp[i - 1][k] + ( k == 1 && (s[i] - '0' == 0) ) < mi ) {
mi = dp[i - 1][k] + ( k == 1 && (s[i] - '0' == 0) );
p = k;
}
}
dp[i][s[i] - '0'] = mi;
pre[i][s[i] - '0'] = p;
}
else if (s[i] == '?') {
for (int j = 0; j <= 1; j++) {
int p = -1, mi = 1 << 30;
for (int k = 0; k <= 1; k++) {
if ( dp[i - 1][k] + ( k == 1 && j == 0 ) < mi ) {
mi = dp[i - 1][k] + ( k == 1 && j == 0 );
p = k;
}
}
dp[i][j] = mi;
pre[i][j] = p;
}
}
}
// std::cout << dp[n][0] << ' ' << dp[n][1] << '\n';
int p = -1, mi = 1 << 30;
for (int i = 0; i <= 1; i++) if (dp[n][i] < mi) {
mi = dp[n][i];
p = i;
}
std::vector<int> ans;
for (int i = n; i >= 1; --i) {
ans.push_back(p);
p = pre[i][p];
}
std::reverse(ans.begin(), ans.end());
int m = ans.size();
for (int i = 0; i < m; i++) std::cout << ans[i]; std::cout << "\n";
}
signed main() {
int _ = 1; std::cin >> _;
while (_--) solve();
return 0;
}
二:
进一步考虑,让 \(s\) 的 \(10\) 尽可能少,不妨考虑一个基于递推思维的经典贪心算法:
- 从左往右扫,更新 \(?\) 为左边一位的字符。
- 除了一定会产生的“相邻不同串” ,\(?\) 不会贡献更多的“不同相邻串。
- 这无关 \(10\) 或 \(01\) 的顺序,仅避免产生“相邻不同串”。因为不考虑顺序,于是可以从任意一边递推。
- 为了把控具体的尽可能少出现的相邻不同串,定义边界即可。
- 当左边界的 \(s_i = ?\) ,让 \(s_i = 0\) ,便不会产生更多的 \(10\) 。
view2
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
void solve(){
std::string s; std::cin >> s; int n = s.length();
for (int i = 0; i < n; i++) if (s[i] == '?') {
s[i] = i == 1 ? '0' : s[i - 1];
}
std::cout << s << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("IO/in", "r", stdin); freopen("IO/out", "w", stdout);
#endif
int _ = 1; std::cin >> _;
while (_--) solve();
return 0;
}