最热泪盈眶的一次,中间被 D 卡了半小时,最后极限过 EF 翻盘。

不过 D 好像就是暴力,只是因为我没开 __int128?(

rk.562。

A - Thermometer

按照题意模拟即可。

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
double x;
int main(){
cin>>x;
if(x>=38)cout<<"1"<<endl;
else if(x>=37.5&&x<38)cout<<"2"<<endl;
else cout<<"3"<<endl;
}

时间复杂度 \(O(1)\)。

B - Ticket Gate Log

按照一个 i 一个 o 的顺序匹配,如果匹配不上,就将 iooi,如果插入的字符数和原字符数为奇数,则应当在末尾再插入一个。

因为一些语法问题 CE 了一发。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
char s[1000];
int len,k,ans;
int main(){
scanf("%s",s+1);
len=strlen(s+1);
for(int i=1;i<=len;i++){
if(!k&&s[i]=='o')i--,ans++;
if(k&&s[i]=='i')i--,ans++;
k^=1;
}
if((len+ans)&1)ans++;
cout<<ans<<endl;
return 0;
}

时间复杂度 \(O(n)\)。

C - Variety Split Easy

我们枚举在那个位置分割,假设当前枚举到了位置 \(i\),要将序列分割为 \([1,i],[i+1,n](i<n)\)。

因为我们在枚举位置 \(i\) 之前就枚举了位置 \(i-1\),所以我们知道 \([1,i-1]\) 中数字种类数,如果加入了的 \(a_i\) 在 \([1,i-1]\) 没有出现过,则 \([1,i]\) 的种类数比 \([1,i-1]\) 种类数多一,具体维护一个桶即可,同理,可以从 \([i,n]\) 转移到 \([i+1,n]\),跟莫队比较类似。

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=3e5+10;
int n,t1[N],t2[N],ans,a[N],res1,res2;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
if(i>1){
if(!t2[a[i]])res2++;
t2[a[i]]++;
}
}
t1[a[1]]++,res1++;
ans=res1+res2;
for(int i=2;i<=n;i++){
if(!t1[a[i]])res1++;
t1[a[i]]++;
t2[a[i]]--;
if(!t2[a[i]])res2--;
ans=max(ans,res1+res2);
}
printf("%d\n",ans);
return 0;
}

时间复杂度 \(O(n)\)。

D - Cubes

没补,纯口胡。

首先存在立方差公式,即:

\[x^3-y^3=(x-y)(x^2+xy+y^2)=n
\]

记 \(t=(x-y)\),变形得:

\[t(t^2+3xy)=t^3+2txy=n
\]

显然,\(t\) 不超过 \(\sqrt[3]{n}\),枚举 \(t\),二分 \(x,y\) 的值。时间复杂度 \(O(\sqrt[3]{n}\log n)\)。

下周一定补(。

E - Path Decomposition of a Tree

很奇妙的一题。也是最后过的题。

没有必要执着于将树划分成 \(n\) 条链,只要可以把树划分成若干条长为 \(k\) 的链,则一定有解。

对于以 \(u\) 为根的子树,若整个子树可以合法分解成链,则有两种情况:

  1. 不考虑子树外的部分,整颗子树可以分解为链。

  2. 子树中 \(u\) 和 \(u\) 的一些子孙和子树外的一些节点被划分成一条链。

显然,若一颗子树能被方式一分解,则一定不可以被方式二分解。反过来也同样如此。

我们先判断一个子树能否按方式一分解,分为以下两种情况。假设 \(u\) 有 \(k\) 个儿子 \(s_1,\dots,s_k\)。

  • 存在一个子孙节点 \(v\),同时属于子节点 \(s_i\) 的子孙节点,满足 \(u\to v\) 路径长为 \(k\),且如果在 \(s_i\) 子树中去除 \(s_i\to v\) 的路径后,剩下的部分可以分解,且其他子儿子节点可以按方式一可以合法分解。

  • 存在两个子孙节点 \(x,y\),同时分别属于子节点 \(s_i,s_j(i\ne j)\) 的子孙节点,满足 \(x\to u\to y\) 的路径长为 \(k\),且如果在 \(s_i,s_j\) 子树中去除 \(s_i\to x,s_j\to y\) 的路径后,剩下的部分可以分解,且其他子儿子节点可以按方式一可以合法分解。

这有什么用呢?我们再看方式二,容易发现:若子树 \(u\) 中存在一个子孙节点 \(v\),满足 \(u\to v\) 的路径去除后剩下的部分可以分解,则整棵树可能有可以分解,但如果无法按照方式一分解,同时无法满足上述条件,则整颗树一定无法分解。

可以在 DFS 的过程中,尽量对 \(u\) 每一颗儿子子树按照方式一分解,若可以分解,则分解下一课子树,若无法彻底分解,则将无法分解的路径挂在 \(u\) 上。最后按照方式一统一处理这些路径,如果处理后仍有超过 \(1\) 条路径没有处理,则整棵树不可能分解;反之,则将剩下的一条路径传回 \(u\) 的父亲。

注意点:

  1. 若不考虑 \(u\) 的前提下,\(u\) 子树的其他部分可以按照方式一分解,则要将 \(u\) 视为一条长为 \(1\) 的路径传会父亲。

  2. 若 \(k=1\),则原树一定可以分解,注意特判(我赛时竟然可以想出 conner cases?)。

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstdlib>
using namespace std;
const int N=2e5+10;
int n,k,m;
vector<int>G[N],v[N];
void add(int x,int y){
G[x].push_back(y);
}
void dfs(int x,int fa){
for(auto y:G[x]){
if(y==fa)continue;
dfs(y,x);
if(!v[y].size())continue;
if(v[y].size()>1){
printf("No\n");
exit(0);
}
v[x].push_back(v[y][0]);
}
if(v[x].size()>3){
printf("No\n");
exit(0);
}else if(v[x].size()==2){
if(v[x][0]+v[x][1]+1==k){
v[x].clear();
}else{
printf("No\n");
exit(0);
}
}else if(v[x].size()==1){
if(v[x][0]+1==k){
v[x].clear();
}else v[x][0]++;
}else if(!v[x].size()){
v[x].push_back(1);
}
return;
}
int main(){
scanf("%d %d",&n,&k);
m=n*k;
if(k==1){
printf("Yes\n");
return 0;
}
for(int i=1,u,v;i<m;i++){
scanf("%d %d",&u,&v);
add(u,v),add(v,u);
}
dfs(1,0);
if(!v[1].size()){
printf("Yes\n");
}else{
printf("No\n");
}
return 0;
}

时间复杂度 \(O(n)\)。最后过的一道题。

F - Variety Split Hard

感谢这道题,绝境中帮我找回信心。下文中,我们称 \(x\) 第一次出现的位置 \(p\) 为序列的数字种类数有贡献。记数字 \(x\) 上一次出现的位置为 \(lst_x\)(若第一次出现则 \(lst_x=0\))。

首先预处理出后缀的数字种类数,然后枚举 \(j\),这下只需要考虑如何找到最优的 \(i\)。

注意到将 \([1,j]\) 划分成 \([1,i],[i+1,j]\)。对于 \([1,j]\) 有贡献的位置,无论被划分到 \([1,i]\) 还是 \([i+1,j]\),都仍然对前两段产生 \(1\) 的贡献,我们称这部分的贡献为基础贡献。但对于 \(1\le lst_{a_{p}}\le i<p\) 的位置 \(p\),则产生的新的贡献。我们称这些位置产生的贡献为额外贡献。

考虑扫描线,当扫到位置 \(j\) 时,线段树上第 \(i\) 个位置上是在 \(i\) 分割产生的额外的贡献,因为基础贡献一定,所以尽可能让额外贡献最大,需查询前缀最大值。对于新加入的位置 \(p\),当分割位置 \(i\) 满足 \(lst_{a_{p}}\le i<p\) 时会产生 \(1\) 的额外贡献,需要区间加。

区间加区间 \(\max\),懒标记线段树即可做到 \(O(n\log n)\),最后加上后缀 \(f_{j+1}\) 即可。

时间复杂度 \(O(n\log n)\)。

点击查看代码
#include <iostream>
#include <cstdio>
#define ls (p<<1)
#define rs (p<<1|1)
using namespace std;
const int N=3e5+10;
struct sgt{
int mx[N<<2],tag[N<<2];
void push_up(int p){
mx[p]=max(mx[ls],mx[rs]);
}
void push_down(int p){
if(!tag[p])return;
mx[ls]+=tag[p],tag[ls]+=tag[p];
mx[rs]+=tag[p],tag[rs]+=tag[p];
tag[p]=0;
}
void build(int p,int l,int r){
mx[p]=tag[p]=0;
if(l==r)return;int mid=(l+r)>>1;
build(ls,l,mid),build(rs,mid+1,r);
}
void change(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R){
mx[p]+=v,tag[p]+=v;
}else{
int mid=(l+r)>>1;push_down(p);
if(L<=mid)change(ls,l,mid,L,R,v);
if(R>mid)change(rs,mid+1,r,L,R,v);
push_up(p);
}
return;
}
int ask(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return mx[p];
int mid=(l+r)>>1,cnt=0;push_down(p);
if(L<=mid)cnt=max(cnt,ask(ls,l,mid,L,R));
if(R>mid)cnt=max(cnt,ask(rs,mid+1,r,L,R));
push_up(p);return cnt;
}
}tr;
int n,f[N],t[N],lst[N],ans,a[N],res;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",a+i);
for(int i=n;i>=1;i--){
if(!t[a[i]])f[i]++;
t[a[i]]++,f[i]+=f[i+1];
}
for(int i=1;i<=n;i++)t[i]=0;
tr.build(1,1,n);
for(int i=1;i<=n;i++){
if(!t[a[i]])res++;
if(lst[a[i]])tr.change(1,1,n,lst[a[i]],i-1,1);
if(i>1)ans=max(ans,f[i+1]+res+tr.ask(1,1,n,1,i-1));
t[a[i]]++,lst[a[i]]=i;
}
printf("%d\n",ans);
return 0;
}

G - Maximize Distance

数据范围很小,考虑网络流。

首先二分 \(d\),转化为判定能否在进行不超过 \(k\) 次操作的前提下,使 \(1\) 到 \(n\) 的最短距离大于 \(d\)。

然后拆点,对于一个点 \(x\),我们将其拆成 \((d+1)\) 个点 \((x,y)\),表示从 \(1\) 到达 \(x\),距离为 \(y\)。对于原图中的任意一条边 \(u\to v\):

  • 对于所有 \(0\le D\le d\),连接一条容量为 \(1\),从 \((u,D)\to (v,D)\) 的边。

  • 对于所有 \(0\le D<d\),连接一条容量为 \(+\infty\),从 \((u,D)\to (v,D+1)\) 的边。

设原点为 \(s\),汇点为 \(t\)。则:

  • 连接一条容量为 \(+\infty\),从 \(s\to (1,0)\) 的边。

  • 对于所有 \(0\le D\le d\),连接一条容量为 \(+\infty\),从 \((n,D)\to t\) 的边。

跑最大流,求出网络的最小割,最小割的值即是使 \(1\) 到 \(n\) 最短路径大于 \(d\) 的最小操作数。


解释一下原理。

要想让 \(1\) 到 \(n\) 的最短距离大于 \(d\),等价于割掉网络中的一些边,使得 \(s,t\) 不联通。

割掉一条边等价于让一条边的权重从 \(0\) 变为 \(1\)。实际上,我们只有可能割掉容量为 \(1\) 的边,即 \((u,D)\to (v,D)\) 的边,这样从 \(u\to v\) 就必须走 \((u,D)\to (v,D+1)\) 这条边。此时一次操作的代价 \(1\) 即和割掉边的容量相同。因为边的边权至多增加 \(1\),所以 \((u,D)\to (u,D+1)\) 这中边的容量是 \(+\infty\),防止被割断。

这几天一直在想的一个问题:\(u\to v\) 这条边在网络中对应 \(D+1\) 条边,理想情况下,最优的方案只会割去其中一条。但最小割中有没有可能割去超过 \(1\) 条边?

答案是否定的,假设在原图中有两条不一样的路径都经过了 \(u\to v\) 的边,且到达 \(u\) 点时移动的距离不一样。在网络中我们割去了 \((u,D_1) \to (v,D_1),(u,D_2)\to (v,D_2)\) 这两条边(\(D_1<D_2\))。我们先思考第一条边 \((u,D_1) \to (v,D_1)\),因为最小割使得 \(s,t\) 不联通,对应在原图中,此时这条路径从 \(v\) 到 \(n\) 的最短距离必然大于 \(d-D_1-1\)。

此时,哪怕第二条边不割,\(v\) 的 \(n\) 的最短路仍然满足 \(d-D_1-1\ge d-D_2\)。也就是说,第二条边割掉和没割都不会有任何改变。所以放弃割掉这条边,可以得到一个更小的割,与条件中的最小割矛盾,故原结论成立。

所以我们安心大胆的跑网络流就对了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=3005,M=60005,inf=1e9+7;
int n,m,k,s,t;
vector<int>G[35];
int ver[N],to[M],nxt[M],val[M],idx=1,d[N],now[N];
queue<int>q;
void clear(){
memset(ver,0,sizeof(ver));
memset(to,0,sizeof(to));
memset(nxt,0,sizeof(nxt));
memset(val,0,sizeof(val));
memset(d,0,sizeof(d));
memset(now,0,sizeof(now));
idx=1;
}
void add(int x,int y,int z){
to[++idx]=y,nxt[idx]=ver[x],ver[x]=idx,val[idx]=z;
to[++idx]=x,nxt[idx]=ver[y],ver[y]=idx,val[idx]=0;
}
int id(int x,int y,int z){
return (x-1)*(z+1)+y;
}
bool bfs(){
while(q.size())q.pop();
for(int i=0;i<=t;i++)d[i]=0,now[i]=ver[i];
q.push(s),d[s]=1;
while(q.size()){
int x=q.front();q.pop();
for(int i=ver[x];i;i=nxt[i]){
if(val[i]&&!d[to[i]]){
int y=to[i];
d[y]=d[x]+1,q.push(y);
if(y==t)return 1;
}
}
}
return 0;
}
int dinic(int x,int flow){
if(x==t||!flow)return flow;
int used=0,f,y;
for(int i=now[x];i;i=nxt[i]){
now[x]=i,y=to[i];
if(val[i]&&d[y]==d[x]+1){
f=dinic(y,min(val[i],flow-used));
if(!f)d[y]=0;
else val[i]-=f,val[i^1]+=f,used+=f;
if(used==flow)break;
}
}
return used;
}
bool check(int d){
clear();s=n*(d+1)+1,t=n*(d+1)+2;
add(s,id(1,0,d),inf);
for(int i=0;i<=d;i++)add(id(n,i,d),t,inf);
for(int i=1;i<=n;i++){
for(auto j:G[i]){
for(int k=0;k<=d;k++){
add(id(i,k,d),id(j,k,d),1);
if(k!=d)add(id(i,k,d),id(j,k+1,d),inf);
}
}
}
int maxflow=0;
while(bfs())maxflow+=dinic(s,inf);
return (maxflow<=k);
}
int main(){
scanf("%d %d %d",&n,&m,&k);
for(int i=1,u,v;i<=m;i++){
scanf("%d %d",&u,&v);
G[u].push_back(v);
}
int l=0,r=m;
while(l<r){
int mid=(l+r)>>1;
if(check(mid))l=mid+1;
else r=mid;
}
printf("%d\n",l);
return 0;
}

有的人认为二分的范围是 \([1,n]\)?不过我写的是 \([1,m]\)。

这样建图点数大概是 \(nm\) 级别,边数则是 \(m^2\) 级别。时间复杂度 \(O(n^2m^4\log n)\),Dinic 轻松应对。


终于补完了。

随机推荐

  1. 揭秘AI自动化框架Browser-use(三):Browser-use控制浏览器的核心机制

    1. 概述 在Browser-use框架中,核心任务是使大模型能够像人类一样操作浏览器.本文深入探讨大模型如何实际控制浏览器,重点解析从模型输出到浏览器动作执行的完整流程. 上一篇(公众号首发)-Br ...

  2. Git命令与Git-cli

    1. 安装Git-cli winget install --id GitHub.cli 注:以上命令用于在Windows平台CMD执行快捷安装,若需安装其他平台的Git-cli及详细内容,win安装文 ...

  3. 可持久化 01-trie 简记

    本文略过了 trie 和 可持久化的介绍,如果没学过请先自学. 在求给定一个值 \(k\) 与区间中某些值的异或最大值时,可以考虑使用在线的数据结构可持久化 01-trie 来维护. 01-trie ...

  4. 【UEFI】PEI阶段从概念到代码

    总述 UEFI开发过程中,BIOS工程师主要关注点和工作都在于PEI和DXE阶段. DXE阶段是我们的主战场,可以进行丰富且大量的功能驱动开发. 实际上,在UEFI Spec中,就指出 ---- Th ...

  5. Go 层级菜单树转 json 处理

    本篇是一个补充知识点, 目的是为了下篇的后台管理系统中, 菜单权限的接口进行铺垫一下. 同时也是做个笔记, 因为在很多地方都会用这种 "树结构" 来实现很多权限, 层级, 菜单的处 ...

  6. .NET 10 Preview 4中ASP.NET Core 改进

    .NET团队在5月13日发布了.NET 10 Preview 4: https://devblogs.microsoft.com/dotnet/dotnet-10-preview-4/.该版本虽无突破 ...

  7. .NET 8 开发的跨平台多商户第三方支付SDK

    前言 快速发展的互联网应用开发中,支付功能已成为各类平台不可或缺的一环.为了帮助大家更高效地接入主流支付渠道,推荐一套基于 .NET 开发的第三方支付 SDK.该 SDK 支持跨平台运行,适用于多种操 ...

  8. B1028 人口普查

    某城镇进行人口普查,得到了全体居民的生日.现请你写个程序,找出镇上最年长和最年轻的人.这里确保每个输入的日期都是合法的,但不一定是合理的--假设已知镇上没有超过200岁的老人,而今天是2014年9月6 ...

  9. git 初始化项目、创建本地分支、本地分支与远程分支关联

    在远程没有项目的场景下,可以通过如下步骤创建和关联远程分支: 在Git官网上点击New repository新建项目: 在本地新建一个同名文件(以demo为例),并初始化项目: 在demo目录打开gi ...

  10. FFmpeg开发笔记(六十二)Windows给FFmpeg集成H.266编码器vvenc

    ​<FFmpeg开发实战:从零基础到短视频上线>该书的第八章介绍了如何在Windows环境给FFmpeg集成H.264和H.265的编码器,如今H.266的编码器vvenc也日渐成熟,从7 ...