08-09 題解
A
小水題
思路
假設我們選定了當前子序列的絕對眾數 \(x\), 那麼該序列裡最多再放 \(num_x - 1\) 個其他數字
為了分出最少的子序列, 肯定要讓每個子序列在擁有絕對眾數的同時能消化儘量多的其他數字
由此, 可以得到一個貪心策略: 每次取出出現次數最多的一個數字, 消掉出現次數最少的那些數字
這個可以排序後雙指標實現, 但是我選擇細節更簡單的線段樹哈哈哈
程式碼
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, INF = 1e18;
int n;
int a[N], maxi, cnt[N];
bool vis[N];
struct Seg_Tree{
struct Node{
int l, r;
int maxi, maxps;
int mini, minps;
}t[2 * N];
void Push_up(int id){
int l = id * 2, r = id * 2 + 1;
if(t[l].maxi > t[r].maxi){
t[id].maxi = t[l].maxi;
t[id].maxps = t[l].maxps;
}else{
t[id].maxi = t[r].maxi;
t[id].maxps = t[r].maxps;
}
if(t[l].mini < t[r].mini){
t[id].mini = t[l].mini;
t[id].minps = t[l].minps;
}else{
t[id].mini = t[r].mini;
t[id].minps = t[r].minps;
}
}
void Build(int id, int l, int r){
t[id].l = l, t[id].r = r;
if(l == r){
if(!cnt[l]){
t[id].maxi = -INF, t[id].maxps = l;
t[id].mini = INF, t[id].minps = l;
}else{
t[id].maxi = cnt[l], t[id].maxps = l;
t[id].mini = cnt[l], t[id].minps = l;
}
return;
}
int mid = (l + r) / 2;
Build(id * 2, l, mid);
Build(id * 2 + 1, mid + 1, r);
Push_up(id);
}
void Modify(int id, int l, int r, int v){
if(r < t[id].l || t[id].r < l){
return;
}
if(l <= t[id].l && t[id].r <= r){
t[id].maxi += v;
t[id].mini += v;
if(!t[id].maxi){
t[id].maxi = -INF;
t[id].mini = INF;
}
return;
}
Modify(id * 2, l, r, v);
Modify(id * 2 + 1, l, r, v);
Push_up(id);
}
}tr;
signed main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
++cnt[a[i]];
maxi = a[i] > maxi ? a[i] : maxi;
}
tr.Build(1, 1, maxi);
int cnt = 0;
while(true){
int mxi = tr.t[1].maxi;
int mxps = tr.t[1].maxps;
if(mxi == -INF) break;
++cnt;
tr.Modify(1, mxps, mxps, -mxi);
int sum = 0, lim = mxi - 1;
while(true){
int mni = tr.t[1].mini;
int mnps = tr.t[1].minps;
if(mni == INF) break;
if(sum + mni <= lim){
sum += mni;
tr.Modify(1, mnps, mnps, -mni);
}else{
int delta = lim - sum;
tr.Modify(1, mnps, mnps, -delta);
break;
}
}
}
cout << cnt << "\n";
}
B
非常巧妙的轉化
思路
每個中心點在 上/下、 左/右 的選擇可以視作獨立的
意思就是在合法(要選擇的點沒有被佔用)的情況下, 上/下 可以任選一個點, 左/右 同理, 相互之間沒有影響
然後設計一種建邊方式, 表示我們在這兩個點(上/下, 左/右)中的選擇
在 上和下, 左和右 之間連一條邊(如果一方不合法, 那就往自己連邊; 如果兩方都不合法, 那就無解), 然後給邊定向
設有向邊 \((u, v)\) 表示我們在 \((u, v)\) 之中選擇了 \(u\), 對此的限制是 : 每個點只能有一個出度(只能被選擇一次)
分連通塊討論, 設當前連通塊點數為 \(n\), 邊數為 \(m\)
- \(m > n\) 無解, 因為每條邊對應一個出度, 這樣每個點不可能只有一個出度
- \(m < n\), 此時因為是連通塊, 所以一定有 \(m = n - 1\), 是一棵樹。 以每個點為根都有一種定向方案, 所以方案數為 \(n\)
- \(m = n\), 這時是一顆基環樹, 分環和樹 考慮。在環上, 有順時針、 逆時針 \(2\) 種方案(自環只有 \(1\) 種, 特判), 在樹上, 每個根節點的入度已經有了, 所以方案固定。 方案數為 \(2\)
完結
程式碼
其實本題暴搜分連通塊考慮, 加上剪枝是可以過的哈哈哈
剪枝程式碼不放了, 這裡是正解
注意並查集大小是 \(n^2\), 在網格圖問題中一定注意陣列是 \(O(n)\) 還是 \(O(n^2)\), 我因為這個掛了 28 pts
#include<bits/stdc++.h>
#define int long long
#define Pii pair <int, int>
using namespace std;
const int N = 1e3 + 10, MOD = 1e9 + 7;
int n, m;
int e[N][N], id[N][N], cid;
char s[N];
struct Ufs{
int s[N * N] ,cnte[N * N], si[N * N], tg[N * N];
void Init(int x){
for(int i = 1; i <= x; i++){
s[i] = i;
cnte[i] = 0;
si[i] = 1;
}
}
int Find(int x){
if(x != s[x]) s[x] = Find(s[x]);
return s[x];
}
}st;
signed main(){
cin >> n >> m;
st.Init(n * m);
for(int i = 1; i <= n; i++){
cin >> (s + 1);
for(int j = 1; j <= m; j++){
e[i][j] = s[j] - '0';
id[i][j] = ++cid;
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(e[i][j] == 1){
int fa = 0, fb = 0;
if(i != 1 && !e[i - 1][j]){
fa = st.Find(id[i - 1][j]);
fb = fa;
}
if(i != n && !e[i + 1][j]){
fb = st.Find(id[i + 1][j]);
if(!fa) fa = fb;
}
if(fa == fb){
++st.cnte[fa];
st.tg[fa] = 1;
}else{
st.si[fa] += st.si[fb];
st.cnte[fa] += st.cnte[fb] + 1;
st.tg[fa] |= st.tg[fb];
st.s[fb] = fa;
}
if(!fa && !fb){
cout << "0\n";
return 0;
}
fa = 0, fb = 0;
if(j != 1 && !e[i][j - 1]){
fa = st.Find(id[i][j - 1]);
fb = fa;
}
if(j != m && !e[i][j + 1]){
fb = st.Find(id[i][j + 1]);
if(!fa) fa = fb;
}
if(!fa && !fb){
cout << "0\n";
return 0;
}
if(fa == fb){
++st.cnte[fa];
st.tg[fa] = 1;
}else{
st.si[fa] += st.si[fb];
st.cnte[fa] += st.cnte[fb] + 1;
st.tg[fa] |= st.tg[fb];
st.s[fb] = fa;
}
}
}
}
int ans = 1;
for(int i = 1; i <= cid; i++){
if(st.Find(i) == i){
int f = st.Find(i);
if(st.si[f] < st.cnte[f]){
cout << "0\n";
return 0;
}
if(st.si[f] == st.cnte[f]){
if(st.tg[f]){
ans *= 1;
}else{
(ans *= 2) %= MOD;
}
}else{
(ans *= st.si[f]) %= MOD;
}
}
}
cout << ans << "\n";
}
C
正解要 CDQ 處理三維偏序, MnZn 寫不出來, 這裡只介紹結論證明
思路
有一個 一眼看上去就不對 的結論: 如果一個區間 \([l, r]\) 有 \(ka\) 個 \(0\) 和 \(kb\) 個 \(1\), 那麼他是可以被刪掉的, 貢獻的次數為 \(k\)
證明:
要證 「若\(len_{l, r} = ka + kb\) 就能刪完」, 只需證 「該區間至少能進行一次刪除」, 然後轉化為 \(len_{l, r} = (k - 1)a + (k - 1)b\) 的子問題
把 \(l, r\) 分成 \(k\) 段長為 \(a + b\) 的段
若有一段內滿足 \(a' = a, b' = b\) 那就直接完事了
否則一定有相鄰的兩段, 一段 \(a' > a\) , 另一端 \(a' < a\), 那一個長度為 \(a + b\) 的滑動視窗放在上面
因為每次 \(a'\) 的變化量為 \(0\) 或 \(1\), 所以 \(a'\) 的變化函式一定是連續的
根據零點存在性定理(好像是必修一的), 由 \(a' > a\) 到 \(a' < a\), 一定會經過 \(a' = a\) 的點, 所以該區間一定能刪至少一次
然後簡單的 \(n^2\) DP 就可以獲得 88 pts
咱也不知道 MX 資料咋造的
程式碼
部分分的
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, INF = 1e18;
int n, a, b;
int sa[N], sb[N], dp[N];
char t[N];
signed main(){
cin >> a >> b >> (t + 1);
n = strlen(t + 1);
for(int i = 1; i <= n; i++){
sa[i] = sa[i - 1];
sb[i] = sb[i - 1];
if(t[i] == '0') ++sa[i];
if(t[i] == '1') ++sb[i];
}
int len = a + b;
for(int i = 1; i <= n; i++){
dp[i] = dp[i - 1];
for(int k = 1; k * len <= i; k++){
if(sa[i] - sa[i - k * len] <= k * a && sb[i] - sb[i - k * len] <= k * b){
dp[i] = dp[i - k * len] + k > dp[i] ? dp[i - k * len] + k : dp[i];
}
}
}
cout << dp[n] << "\n";
}
D - P9514
思路
要往模 \(d\) 的餘數上考慮, 這是一定的
還發現我們的矩陣最大就是 \(d * d\) 的, 這樣一定可以囊括所有的圖案
先觀察 I, II 類圖案的排列方式
I 的就是從初始點開始, 橫座標每隔 d 有一個, 縱座標每隔 d 有一個(可能表述不嚴謹, 可以看題面的樣例解釋), 所以我們的 \(d * d\) 矩陣包含且僅能包含一個
II 的就是每隔 d 有一列, 每隔 d 有一行
要我們求一個類似最大子矩陣的問題, 複雜度在 \(n^2\) 左右, 考慮分別列舉 \(x, y\) 方向
先列舉 \(l_x\), 根據上面的觀察, 貪心地來選 \(r_x\) 最小是 \([l, l + d - 1]\) 中最晚出現的 I 類圖案的橫座標
然後再考慮 \(y\) 方向, 這裡只需考慮 \(mod\ d\) 意義下的, 在 \(y\) 軸上構成一個環狀結構(使用滑動視窗, 在 \(y\) 軸上取出任意一段長度為 \(d\) 的都可以), 最小的長度就是環上斷開一條最長的、 上面沒有圖案的邊
對於每一個「不被 I 類圖案覆蓋、 並且 \(S_j\) 等於他的所有的 II 類圖案都已經被第一種方案(\(R_j\) 覆蓋完畢了」 的 \(y\) , 我們可以把他刪掉, 否則存到對應的最大的 \(R_j\)中, 因為這個 II 類圖案我們還沒有滿足, 還要用到這個 \(y\)
再考慮擴大 \(r\) , 使得剩下的 II 類也被滿足
從小往大列舉 \(r\), 每次刪掉已經滿足條件的 \(y\) , 取 min 即可
程式碼
#include<bits/stdc++.h>
#define int long long
#define Pii pair <int, int>
using namespace std;
const int N = 5e3 + 10, M = 5e5 + 10, INF = 1e9;
int n, m, d;
int p[M], q[M], r[M], s[M];
int lstps[N][N * 2];
bool visx[N], visy[N];
vector <int> ps[N * 2];
struct List{
int pre[N], nxt[N], len, hd, tl;
void Init(){
for(int i = 0; i < d; i++){
pre[i] = i - 1;
nxt[i] = i + 1;
}
hd = 0, tl = d - 1;
len = INF;
}
void Del(int x){
int l = pre[x], r = nxt[x];
if(l != -1) nxt[l] = r;
if(r != d) pre[r] = l;
if(pre[r] == -1) hd = r;
if(nxt[l] == d) tl = l;
if(l != -1 && r != d){
len = min(len, d - (r - l) + 1);
}
}
}lst;
signed main(){
cin >> n >> m >> d;
for(int i = 1; i <= n; i++){
cin >> p[i] >> q[i];
visx[p[i]] = 1;
visy[q[i]] = 1;
}
for(int i = 1; i <= m; i++){
cin >> r[i] >> s[i];
lstps[s[i]][r[i]] = r[i];
lstps[s[i]][r[i] + d] = r[i] + d;
}
for(int i = 0; i < d; i++){
for(int j = 1; j < 2 * d; j++){
lstps[i][j] = max(lstps[i][j], lstps[i][j - 1]);
}
}
int ans = d * d;
for(int l = 0; l <= d; l++){
int r = 0;
for(int j = l + d - 1; j >= 0; j--){
if(visx[j % d]){
r = j;
break;
}
}
lst.Init();
for(int y = 0; y < d; y++){
if(!visy[y]){
if(lstps[y][l + d - 1] < r){
lst.Del(y);
}else{
ps[lstps[y][l + d - 1]].push_back(y);
}
}
}
for(int j = r; j <= l + d - 1; j++){
for(auto k : ps[j]){
lst.Del(k);
}
ans = min(ans, (j - l + 1) * min(lst.len, (lst.tl - lst.hd + 1)));
ps[j].clear();
}
}
cout << ans << "\n";
}