ABC397
最热泪盈眶的一次,中间被 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
的顺序匹配,如果匹配不上,就将 i
变 o
或 o
变 i
,如果插入的字符数和原字符数为奇数,则应当在末尾再插入一个。
因为一些语法问题 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
没补,纯口胡。
首先存在立方差公式,即:
\]
记 \(t=(x-y)\),变形得:
\]
显然,\(t\) 不超过 \(\sqrt[3]{n}\),枚举 \(t\),二分 \(x,y\) 的值。时间复杂度 \(O(\sqrt[3]{n}\log n)\)。
下周一定补(。
E - Path Decomposition of a Tree
很奇妙的一题。也是最后过的题。
没有必要执着于将树划分成 \(n\) 条链,只要可以把树划分成若干条长为 \(k\) 的链,则一定有解。
对于以 \(u\) 为根的子树,若整个子树可以合法分解成链,则有两种情况:
不考虑子树外的部分,整颗子树可以分解为链。
子树中 \(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\) 的父亲。
注意点:
若不考虑 \(u\) 的前提下,\(u\) 子树的其他部分可以按照方式一分解,则要将 \(u\) 视为一条长为 \(1\) 的路径传会父亲。
若 \(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 轻松应对。
终于补完了。
随机推荐
- 强烈推荐!终于找到了一个查看 dotnet 源码的便捷方法
强烈推荐!终于找到了一个查看 dotnet 源码的便捷方法 快把这个网站加入标签栏 "https://source.dot.net/" 为什么推荐 由GitHub生成,由Ro ...
- KoalaWiki vs DeepWiki:更优秀的开源代码知识库解决方案
KoalaWiki vs DeepWiki:更优秀的开源代码知识库解决方案 资源链接: 教程代码仓库:https://github.com/AIDotNet/SemanticKernel.Sample ...
- windows查看连接过wifi的密码
如何使用批处理脚本获取已保存的WiFi密码 在日常使用电脑时,我们经常会连接多个WiFi网络.随着时间的推移,可能会忘记某些WiFi的密码.幸运的是,Windows系统提供了命令行工具netsh,可以 ...
- 【安装】Linux下安装CUDA ToolKit 11.4和cuDNN 8
注意!如果你使用的是pytorch,只需要装好CUDA,不需要装cuDNN.而且完全可以等到报错了再装CUDA,一般情况系统都已经装好CUDA Toolkit了. 除非你只装了低版本的CUDA Too ...
- 一文玩转NGINX(对于NGINX,你真的了解吗?)
前言 nginx [engine x] 是一个 HTTP 和反向代理服务器, 邮件代理服务器, 和一个通用的 TCP/UDP 代理服务器, 最初由 Igor Sysoev. 运行了很久 在许多负载重的 ...
- TVM:使用张量表达式处理算子
在本教程中,把注意力转向 TVM 如何使用张量表达式(Tensor Expression,简称 TE)定义张量计算并应用循环优化.TE 以纯函数式语言描述张量计算(即每个表达式都没有副作用).从 TV ...
- WPF 窗口 触摸失效 的一种场景
最近,生产线反馈,在执行生产大屏测试软件的时候,软件大概率出现不能触摸,但是可以用鼠标的的情况.刚好 这个软件又是WPF 做的,所以做了以下排查. .Net 环境: .NetFrameWork 4.8 ...
- React-Native开发鸿蒙NEXT-图片上传
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...
- Gin?有这一篇就够了!
Gin Gin是Golang的一个后端框架,封装比较优雅,API友好. go get -u github.com/gin-gonic/gin 1.hello word package main imp ...
- Java集合框架性能特征与使用场景深度解析
Java 集合框架的性能优化与场景适配是高级程序员面试的核心考点.本文聚焦线性集合.集合.映射等核心组件的性能指标(时间复杂度.空间开销)与适用场景,结合 JDK 演进特性与工程实践,构建系统化知识体 ...