08-09 题解

A

小水题

思路

假设我们选定了当前子序列的绝对众数 x, 那么该序列里最多再放 numx1 个其他数字

为了分出最少的子序列, 肯定要让每个子序列在拥有绝对众数的同时能消化尽量多的其他数字

由此, 可以得到一个贪心策略: 每次取出出现次数最多的一个数字, 消掉出现次数最少的那些数字

这个可以排序后双指针实现, 但是我选择细节更简单的线段树哈哈哈

代码

#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

  1. m>n 无解, 因为每条边对应一个出度, 这样每个点不可能只有一个出度
  2. m<n, 此时因为是连通块, 所以一定有 m=n1, 是一棵树。 以每个点为根都有一种定向方案, 所以方案数为 n
  3. m=n, 这时是一颗基环树, 分环和树 考虑。在环上, 有顺时针、 逆时针 2 种方案(自环只有 1 种, 特判), 在树上, 每个根节点的入度已经有了, 所以方案固定。 方案数为 2

完结

代码

其实本题暴搜分连通块考虑, 加上剪枝是可以过的哈哈哈

剪枝代码不放了, 这里是正解

注意并查集大小是 n2, 在网格图问题中一定注意数组是 O(n) 还是 O(n2)我因为这个挂了 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]ka0kb1, 那么他是可以被删掉的, 贡献的次数为 k

证明:

要证 「若lenl,r=ka+kb 就能删完」, 只需证 「该区间至少能进行一次删除」, 然后转化为 lenl,r=(k1)a+(k1)b 的子问题

l,r 分成 k 段长为 a+b 的段

若有一段内满足 a=a,b=b 那就直接完事了

否则一定有相邻的两段, 一段 a>a , 另一端 a<a, 那一个长度为 a+b 的滑动窗口放在上面

因为每次 a 的变化量为 01, 所以 a 的变化函数一定是连续的

根据零点存在性定理(好像是必修一的), 由 a>aa<a, 一定会经过 a=a 的点, 所以该区间一定能删至少一次

然后简单的 n2 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 的余数上考虑, 这是一定的

还发现我们的矩阵最大就是 dd 的, 这样一定可以囊括所有的图案

先观察 I, II 类图案的排列方式

I 的就是从初始点开始, 横坐标每隔 d 有一个, 纵坐标每隔 d 有一个(可能表述不严谨, 可以看题面的样例解释), 所以我们的 dd 矩阵包含且仅能包含一个

II 的就是每隔 d 有一列, 每隔 d 有一行

要我们求一个类似最大子矩阵的问题, 复杂度在 n2 左右, 考虑分别枚举 x,y 方向

先枚举 lx, 根据上面的观察, 贪心地来选 rx 最小是 [l,l+d1] 中最晚出现的 I 类图案的横坐标

然后再考虑 y 方向, 这里只需考虑 mod d 意义下的, 在 y 轴上构成一个环状结构(使用滑动窗口, 在 y 轴上取出任意一段长度为 d 的都可以), 最小的长度就是环上断开一条最长的、 上面没有图案的边

对于每一个「不被 I 类图案覆盖、 并且 Sj 等于他的所有的 II 类图案都已经被第一种方案(Rj 覆盖完毕了」 的 y , 我们可以把他删掉, 否则存到对应的最大的 Rj中, 因为这个 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";
}
posted on   Bubble_e  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话



点击右上角即可分享
微信分享提示