比赛 :Educational Codeforces Round 189 (Rated for Div. 2) · 题目链接 :C. Red-Black Pairs
题目大意
题面
有一张 2 × n 2 \times n 2 × n 的格子表,每个格子涂红色 ® 或黑色 (B)。要重涂最少多少个格子,使得能把全部格子分成 n n n 对,满足:
输出最少重涂次数。
输入格式
第一行 t t t ( 1 ≤ t ≤ 1 0 4 1 \le t \le 10^4 1 ≤ t ≤ 1 0 4 )组测试。
每组:
约束: ∑ n ≤ 2 ⋅ 1 0 5 \sum n \le 2 \cdot 10^5 ∑ n ≤ 2 ⋅ 1 0 5 。
样例
输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 5 1 R B 2 BR BR 3 RBR BRB 4 RRBB BBRB 5 RBRBR BBBRB
输出:
第 4 个样例一种合法重涂是
思路讲解
一句话
每对配对就是一块 1 × 2 1 \times 2 1 × 2 的多米诺骨牌; 2 × n 2 \times n 2 × n 网格用骨牌完整铺满只有两种局部砖型——整列竖砖 或 相邻两列上下各一块横砖 。线性 DP 一遍扫过去就行, O ( n ) O(n) O ( n ) 。
关键观察:配对结构 ≡ 2×n 多米诺铺法
题目要求「每对同色且相邻」。把「相邻」这一几何约束抽出来看,每对就是一块 1 × 2 1 \times 2 1 × 2 的 多米诺骨牌 。问题立刻翻译成:
把 2 × n 2 \times n 2 × n 的网格用 n n n 块 1 × 2 1 \times 2 1 × 2 多米诺骨牌完全铺满。每块骨牌覆盖两个格子,要求两格同色——不同色就重涂一格,cost = 1。
而 2 × n 2 \times n 2 × n 网格的多米诺铺法是经典结构:任意一种铺法,每一列要么是一块整列竖砖,要么和相邻列形成「上下两块横砖」的组合 。
横砖一定 成对出现 ——上行的横砖跨第 i i i 到第 i + 1 i+1 i + 1 列时,下行同 i i i 到 i + 1 i+1 i + 1 列也必须放一块横砖;不能上行横、下行竖混搭,否则下行第 i i i 列就剩一个孤立格子接不上。
殊途同归地,原题的「重涂使可配对」就成了:枚举所有 2×n 多米诺铺法,选总 cost 最低那一种 。铺法数线性可枚举,于是 DP。
DP 转移
设 d p [ i ] \mathrm{dp}[i] d p [ i ] = 前 i i i 列已经合法铺完的最少重涂次数。两种转移:
1) 第 i i i 列放整列竖砖 :cost = (上下两格不同色 ? 1 : 0)
d p [ i ] = d p [ i − 1 ] + [ s 0 [ i ] ≠ s 1 [ i ] ] \mathrm{dp}[i] \;=\; \mathrm{dp}[i-1] + \bigl[\,s_0[i] \ne s_1[i]\,\bigr]
d p [ i ] = d p [ i − 1 ] + [ s 0 [ i ] = s 1 [ i ] ]
2) 第 i − 1 , i i-1, i i − 1 , i 两列各放一块横砖 (上下行各一块):上行 cost + 下行 cost
d p [ i ] = d p [ i − 2 ] + [ s 0 [ i − 1 ] ≠ s 0 [ i ] ] + [ s 1 [ i − 1 ] ≠ s 1 [ i ] ] \mathrm{dp}[i] \;=\; \mathrm{dp}[i-2] + \bigl[\,s_0[i-1] \ne s_0[i]\,\bigr] + \bigl[\,s_1[i-1] \ne s_1[i]\,\bigr]
d p [ i ] = d p [ i − 2 ] + [ s 0 [ i − 1 ] = s 0 [ i ] ] + [ s 1 [ i − 1 ] = s 1 [ i ] ]
两种转移取 min。边界 d p [ 0 ] = 0 \mathrm{dp}[0] = 0 d p [ 0 ] = 0 (空网格已铺好),答案 d p [ n ] \mathrm{dp}[n] d p [ n ] 。
验证:样例 5
n = 5 n = 5 n = 5 ,上行 s 0 s_0 s 0 = RBRBR ,下行 s 1 s_1 s 1 = BBBRB :
i
竖砖 cost
横砖 cost (上+下)
dp[i]
1
R≠B → 1
—
0 + 1 = 1
2
B = B → 0
R≠B + B = B → 1 + 0
min(1+0, 0+1) = 1
3
R≠B → 1
B≠R + B = B → 1 + 0
min(1+1, 1+1) = 2
4
B≠R → 1
R≠B + B≠R → 1 + 1
min(2+1, 1+2) = 3
5
R≠B → 1
B≠R + R≠B → 1 + 1
min(3+1, 2+2) = 4
输出 4 ✓
复杂度
时间 O ( n ) O(n) O ( n ) ,空间 O ( n ) O(n) O ( n ) (dp 数组其实只用最近两项可以滚到 O ( 1 ) O(1) O ( 1 ) ,但 n ≤ 2 ⋅ 1 0 5 n \le 2 \cdot 10^5 n ≤ 2 ⋅ 1 0 5 完全没必要省)。
AC 代码
AC 提交:373262009
核心 Solve() 就 7 行有效代码,DP 部分直接照公式翻译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void Solve () { ll N; cin >> N; vector<array<char , 2> > s (N); for (int i = 0 ; i < N; ++i) cin >> s[i][0 ]; for (int i = 0 ; i < N; ++i) cin >> s[i][1 ]; vector<ll> dp (N + 1 , INF) ; dp[0 ] = 0 ; for (int i = 0 ; i < N; ++i) { dp[i + 1 ] = min (dp[i + 1 ], dp[i] + (s[i][0 ] != s[i][1 ])); if (i >= 1 ) { dp[i + 1 ] = min (dp[i + 1 ], dp[i - 1 ] + (s[i][0 ] != s[i - 1 ][0 ]) + (s[i][1 ] != s[i - 1 ][1 ])); } } cout << dp[N] << "\n" ; }
展开完整 C++ 源码(含调试模板)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <bits/stdc++.h> #define all(vec) vec.begin(),vec.end() #define lson(o) (o<<1) #define rson(o) (o<<1|1) #define SZ(a) ((long long) a.size()) #define fsp(x) fixed<<setprecision(x) using namespace std;using ll = long long ;using ull = unsigned long long ;using DB = double ;using i128 = __int128;using CD = complex<double >;static constexpr ll MAXN = (ll) 1e6 + 10 , INF = (1ll << 61 ) - 1 ;static constexpr ll mod = 998244353 ;static constexpr double eps = 1e-8 ;const double PI = acos (-1.0 );ll lT, testcase; void Solve () { ll N; cin >> N; vector<array<char , 2> > s (N); for (int i = 0 ; i < N; ++i) cin >> s[i][0 ]; for (int i = 0 ; i < N; ++i) cin >> s[i][1 ]; vector<ll> dp (N + 1 , INF) ; dp[0 ] = 0 ; for (int i = 0 ; i < N; ++i) { dp[i + 1 ] = min (dp[i + 1 ], dp[i] + (s[i][0 ] != s[i][1 ])); if (i >= 1 ) { dp[i + 1 ] = min (dp[i + 1 ], dp[i - 1 ] + (s[i][0 ] != s[i - 1 ][0 ]) + (s[i][1 ] != s[i - 1 ][1 ])); } } cout << dp[N] << "\n" ; } signed main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); cin >> lT; for (testcase = 1 ; testcase <= lT; ++testcase) Solve (); return 0 ; }
心路历程(贪心 → DP)
第一版:邻居贪心(弃用)
刚开始想的是 局部贪心 :扫每个格子,看它上下左右四个邻居有没有同色的——如果一个同色邻居都没有,就把它强制改成某个异色邻居的颜色,ans++。
代码挂在 C_Red_Black_Pairs.cpp :
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int x = 0 ; x < 2 ; ++x) { for (int y = 0 ; y < N; ++y) { ll cnt = 0 ; char ch; for (int i = 0 ; i < 4 ; ++i) { ll tox = x + dx[i], toy = y + dy[i]; if (check (tox, toy)) { if (s[tox][toy] == s[x][y]) ++cnt; else ch = s[x][y]; } } if (!cnt) { s[x][y] = ch; ++ans; } } }
这个贪心 是错的 。问题在于:
第二版:DP(AC)
意识到结构本质就是 2×n 多米诺骨牌铺满 之后,DP 的状态和转移就一目了然——只剩两种砖型,按列扫一遍,AC(373262009 )。
教训
看到「两行 / 网格配对」时的第一反应应该是 多米诺铺法 ,而不是「邻居贪心」——前者是结构化的(铺法只有两种局部规则),后者是临时拼凑的,没法保证全局最优。
附件
暂无(题目较短,没写 mentor.tex 录稿;动画也没做)。