CF2161
CF2161B Make Connected
赛时看错题了。是只能出现连续两个,又是出现连续三个感觉很不可做啊。
最后的结论是,这个 # 的点要么形成 2 乘 2 的正方形,要么形成一个 L 形。
这种斜着的往往跟 \(x - y\),\(x + y\) 什么的有关系,所以一种很优雅的判断方式就是判断一下 \(\max(x - y) - \min(x - y) \le 1\) 之类的。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 5;
typedef pair<int, int> pii;
int n, mnx, mny, mxs, mns, mxm, mnm;
char c[N][N];
vector<pii> pos;
void solve(){
cin >> n;
pos.clear();
mxs = mxm = -1e9, mnm = mns = mnx = mny = 1e9;
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
cin >> c[i][j];
if(c[i][j] == '#'){
pos.emplace_back(i, j);
mnx = min(i, mnx), mny = min(j, mny);
mns = min(mns, i + j), mxs = max(mxs, i + j);
mnm = min(mnm, j - i), mxm = max(mxm, j - i);
}
}
}
if(mxs - mns <= 1 || mxm - mnm <= 1){
return cout << "Yes\n", void();
}
if(pos.size() > 4){
cout << "No\n";
return;
}
// cout << mny << '\n';
for(auto [x, y] : pos){
x -= mnx, y -= mny;
// cout << x << ' ' << y << '\n';
if(!(x >= 0 && x <= 1 && y >= 0 && y <= 1)) return cout << "No\n", void();
}
cout << "Yes\n";
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
CF2161D Locked Out
如果 \(i < j \land a_i + 1 = a_j\) 就把 \((i,j)\) 连边,然后就是一个最大点独立集问题。但我们知道这样还是不好做。考虑按值域一层一层考虑,这一层的决策只跟上一层有关,并且发现我们总是选择一段前缀。那么按此设计 \(f_{i, j}\) 表示第 \(i\) 层选了前 \(j\) 个,\(\le i\) 的最大点独立集。直接转是 \(N^2\) 的,前缀/后缀 max 优化一下,再加个双指针可以做到 \(O(N)\),下面写的是二分。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
vector<int> pos[N], pre[N], suf[N], dp[N];
int n, a[N], ans;
void solve(){
cin >> n;
for(int i = 1; i <= n + 1; ++i){
pos[i].clear(), pre[i].clear(), suf[i].clear(), dp[i].clear();
pos[i].emplace_back(0);
}
for(int i = 1; i <= n; ++i){
cin >> a[i];
pos[a[i]].emplace_back(i);
}
for(int i = 1; i <= n; ++i){
int siz = pos[i].size();
pre[i].resize(siz), suf[i].resize(siz), dp[i].resize(siz);
for(int j = 0; j < siz; ++j){
int p = pos[i][j];
int x = upper_bound(pos[i - 1].begin(), pos[i - 1].end(), p) - pos[i - 1].begin();
if(x > 0) dp[i][j] = max(dp[i][j], pre[i - 1][x - 1]);
if(x < pos[i - 1].size()) dp[i][j] = max(dp[i][j], suf[i - 1][x] - (x - 1));
dp[i][j] += j;
}
for(int j = 0; j < siz; ++j)
pre[i][j] = max((j == 0 ? 0 : pre[i][j - 1]), dp[i][j] - j);
for(int j = siz - 1; j >= 0; --j)
suf[i][j] = max((j == siz - 1 ? 0 : suf[i][j + 1]), dp[i][j]);
}
cout << n - suf[n][0] << '\n';
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
CF2161E Left is Always Right
如果记 \(d\) 是 \([i, i + k - 1]\) 中 1 的个数 - 0 的个数。发现当 \(i \gets i + 1\) 时,\(d \gets d + 2/d/d - 2\)。因此当一个地方 \(i \to i + 1\) 出现 \(1 \to 0\) 时,前面的 \(d\) 肯定是 1,后面的 \(d\) 肯定是 -1,并且 \(i + k\) 这个位置肯定是 0。
然后就卡在这了。实际上再往后模一步会发现,如果下一位继续放 0,那么 \(i + 1 + k\) 也得放 0,如果下一位放 1,那么 \(i + 1 + k\) 也得放 1。发现有周期性,循环节长度是 \(k - 1\),也就是从此之后的所有位置都确定了。
那么考虑枚举前缀 1 的长度 \(i\)(如果第一位是 0 就枚举 0,对称的,第一位是 ? 的话就赋值之后都做一遍),这意味着 \(i + 1\) 是 0。那么此时要保证 \(d_{[i, i + k - 1]} = 1\),也就是保证 \([i + 1, i + k - 1]\) 中 1 的个数 - 0 的个数为 0,也即 1 和 0 都放 \(\frac{k - 1}{2}\) 个。但由于周期性,后面的数有可能已经确定了,并影响前面的决策。这要求后面所有模 \(k - 1\) 同余的位置的取值都相同。然后维护一些桶判断是否冲突,然后再对没有确定的位置进行计数即可(简单组合数)。
最后 \(k\) 位题目的限制直接没了,有一些 corner。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5, mod = 998244353;
int n, k, a[N];
int fl;
map<int, int> cnt[N], num;
ll fac[N], ifac[N], inv[N];
void erase(int i){
if(a[i] == 0) return;
int y = i % (k - 1), &x = cnt[y][a[i]];
--x;
if(x == 0){
cnt[y].erase(a[i]);
if(cnt[y].size() == 1) fl--, num[cnt[y].begin() -> first]++;
else num[a[i]]--;
}
}
void add(ll &x, ll y){ (x += y) %= mod; }
ll C(int x, int y){
if(y > x || y < 0) return 0;
return fac[x] * ifac[y] % mod * ifac[x - y] % mod;
}
void solve(){
cin >> n >> k;
for(int i = 1; i <= n; ++i){
char c; cin >> c;
if(c == '0') a[i] = -1;
else if(c == '1') a[i] = 1;
else a[i] = 0;
}
auto cal = [](){
ll ret = 0;
if(a[1] == -1) for(int i = 1; i <= n; ++i) a[i] *= -1;
for(int i = 0; i < k; ++i) cnt[i].clear();
for(int i = 1; i <= n; ++i) if(a[i] != 0) cnt[i % (k - 1)][a[i]]++;
fl = 0; num.clear();
for(int i = 0; i < k - 1; ++i){
if(cnt[i].size() == 2) fl++;
else if(!cnt[i].empty()) num[cnt[i].begin() -> first]++;
}
int d = k - 1;
for(int i = 1; i <= n - k + 1; ++i){
if(a[i] != -1 && i == n - k + 1){
int n0 = 0, n1 = 0;
for(int j = i; j <= n; ++j){
if(a[j] == 1) n1++;
else if(a[j] == -1) n0++;
}
n1 += a[i] == 0;
for(int x = max(n1, (k + 1) / 2); x <= n; ++x){
add(ret, C(k - n0 - n1, x - n1));
}
}
if(a[i] != 1 && fl == 0){
int x = i % (k - 1);
if(cnt[x].size() == 2 || (!cnt[x].empty() && cnt[x].begin() -> first == 1));
else{
int num0 = num[-1] + cnt[x].empty(), num1 = num[1];
add(ret, C(d - num0 - num1, d / 2 - num0));
}
}
if(a[i] == -1) break;
erase(i);
}
return ret;
};
ll ans = 0;
if(a[1] == 0){
for(int o : {1, -1}){
a[1] = o; add(ans, cal());
}
}
else ans = cal();
cout << ans << '\n';
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
fac[0] = ifac[0] = fac[1] = ifac[1] = inv[1] = 1;
for(int i = 2; i <= 1e5; ++i){
fac[i] = (fac[i - 1] * i) % mod;
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
ifac[i] = (ifac[i - 1] * inv[i]) % mod;
}
int T; cin >> T;
while(T--) solve();
return 0;
}
CF2161F SubMST
考虑所选点集的最小斯坦纳树(就是虚树),如果最小斯坦纳树上的点都在原点集中,那么贡献就是虚树上的边权和。
这一部分的贡献是 $ \sum_{(u, v)} (2^{siz_u} - 1)(2^{siz_v} - 1)$。
接着考虑虚点 \(s\) 不在原点集中的情况。本来一个点 \(u\) 连到 \(s\) 就行了,现在我们发现它还要多走一些连到最近的点集中的点 \(x\)。也就是说,对于虚树上所有与 \(u\) 相邻的点,除了离 \(u\) 最近的点 \(x\),都得多走 \(dis(u, x)\)。然后 \(dis(u, x)\) 在原树上本来也被贡献了一次,也就是多 \((deg_u - 2)dis(u, x)\),发现这样能保证联通而且最小。注意考察一下有重叠的情况,我们总是钦定一个方向走树上的原边,其余方向算多的,然后中转多次的情况就是拼起来,因此这么贡献没问题。
那么我们考虑枚举每个点 \(u\) 不在点集中,然后钦定深度 dp,把所有方案的 \(deg_u\) 之和算出来即可。因为直接写不好写,所以加了个容斥,算最小深度 \(\ge d\) 的。

浙公网安备 33010602011771号