比赛链接
通过顺序:\(B\rightarrow D\rightarrow I\rightarrow J\rightarrow G\rightarrow H \rightarrow A \rightarrow K\rightarrow C\rightarrow E \rightarrow L \rightarrow F\)
A 蹄球锦标赛:
给你\(N\)个点\(N\)条有向边,求在某些点开始跑边能够遍历整张图的最少点数
这已经是简化版的题意了,略去了前面没必要讲的建边(因为对于一个能看懂并写出AC代码的大佬来说这不是啥问题),建边唯一的坑点就是任意一头牛的左右两头牛距离相同连向左边(原题面括号那一句写错了)。
具体讲后面的操作:
如果一个点\(x\)连向\(y\)一条有向边,那么显然放在\(x\)比放在\(y\)更优,因为放\(x\)一定到\(y\),放\(y\)不一定能到\(x\)。
我们不妨做出一个假设:只有没有有向边连到的点是需要放球的
这已经非常接近正解了,但是很可惜,少考虑了一种情况
我们考虑这组数据
4
1 3 1 4
这样子相当于构造出了两个大小为二的环(事实上本题最大环大小就是二,但这不是重点,所以读者可以自证)
这样子显然要输出\(2\),但是我们刚刚那样写会输出\(0\)
我们意识到应该把所有的环消除掉(比如缩成一个点)
所以我们使用tarjan缩点(可以参考洛谷或者其他OJ的题目,这里不过多说明)将图变为DAG(有向无环图)
然后统计一下有多少入度为\(0\)的点即可
时间复杂度:建边\(O(n log n)\),缩点\(O(n)\)
总时间\(O(n log n)\)
\(ACcode:by\space WAduck\)(可能有多余变量或数组,直接无视就好了)
#include<iostream>
#include<queue>
#include<algorithm>
#define maxn 100001
#define t edge[i].to
#define s edge[i].nex
using namespace std;
struct Edge
{
int to,nex;
}edge[maxn],edg[maxn];
int st[maxn],dis[maxn],DFN[maxn],LOW[maxn],dye[maxn],vis[maxn],size[maxn],stack[maxn],sta[maxn],degree[maxn],dist[maxn],x1[maxn],y2[maxn];
int n,m,cnt,stacksize,color,dfsnum,c,a[2001],maxx;
void addedge(int x,int y)
{
edge[++cnt].to=y;
edge[cnt].nex=st[x];
st[x]=cnt;
}
void tarjan(int start)
{
vis[stack[++stacksize]=start]=1;
LOW[start]=DFN[start]=++dfsnum;
for(int i=st[start];i;i=s)
if(!DFN[t])tarjan(t),LOW[start]=min(LOW[start],LOW[t]);
else if(vis[t])LOW[start]=min(LOW[start],DFN[t]);
if(DFN[start]==LOW[start])
{
int y;
while(y=stack[stacksize--])
{
dye[y]=start;
vis[y]=0;
if(start==y)break;
dis[start]+=dis[y];
}
}
}
int main()
{
cin>>n;
m=n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
sort(a+1,a+1+n);
addedge(1,2);
x1[1]=1;y2[1]=2;
x1[n]=n;y2[n]=n-1;
addedge(n,n-1);
for(int i=2;i<n;i++)
{
if(a[i]-a[i-1]<=a[i+1]-a[i])addedge(i,i-1),x1[i]=i,y2[i]=i-1;
else addedge(i,i+1),x1[i]=i,y2[i]=i+1;
}
for(int i=1;i<=n;i++)if(!DFN[i])tarjan(i);
for(int i=1;i<=m;i++)
{
int x=dye[x1[i]],y=dye[y2[i]];
maxx=max(maxx,max(x,y));
//cout<<x1[i]<<' '<<y1[i]<<' '<<x<<' '<<y<<endl;
if(x!=y)
{
edg[++c].to=y;
edg[c].nex=sta[x];
sta[x]=c;
degree[y]++;
}
}
int ans=0;
for(int i=1;i<=maxx;i++)
{
//cout<<degree[i]<<endl;
if(!degree[i]&&dye[i]==i)ans++;
}
cout<<ans;
return 0;
}
B:便便传送门(一)
(有味道的题)
这里的传送门可以双向传送,但是我们最多使用一次传送门就够了(显而易见读者自证)
我们要分三种情况讨论
一,不使用传送门
答案为两点的直线距离\(\left|a-b\right|\)
二,使用传送门从左边到右边
答案为起点到左侧传送门的直线距离加上右侧传送门到终点的直线距离
三,使用传送门从右边到左边
与二同理
最后输出三种情况的最小值即可
\(ACcode:by \space kan\_kiz\)
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
int main()
{
int maxn=99999999;
int a,b,x,y;
scanf("%d%d%d%d",&a,&b,&x,&y);
maxn=min(abs(x-a)+abs(y-b),min(abs(x-b)+abs(y-a),abs(b-a)));
printf("%d",maxn);
return 0;
}
C: 便便传送门(二)
根据上一道题的公式,我们能很快的概括题面:
选取一个值\(x\),使\(\sum^{n}_{i=1}min(\left| a[i]-b[i]\right|,\left|a[i]\right|+\left|b[i]-x\right|)\)的值最小
解法一:(瞎搞,期望得分:\(20\))
我们发现最优的\(x\)会出现在一段(或多段)区间中,并且在一定范围内满足单调性,我们可以使用模拟退火(什么鬼),但是显然最优答案区间可以构造长度为\(1\),而且ACM赛制就别想着骗分了
正解:
其实想到正解是很意外的,首先是\(yxy\)打听到是什么斜率(她说的是斜率优化)
然后那天我又无所事事地在玩函数图像生成器
然后我想到了把式子带进去
结果是很惊人的
有一些函数成了常函数,有一些则是线性分段函数
分段函数的交点还是整点,我当时就明白了怎么写2333
首先我们要求函数的交点坐标
一,首先我们讨论什么时候是常函数
答案是显然的,走到传送门的距离比直接到终点的距离还长,那么纵坐标\(y= \left|a-b\right|\)
二,由于考虑到传送门是单向传送的,所以我们还有两种情况需要讨论
对于第一种情况,\(a\)去\(b\)的路上经过传送门入口(也可以说是\(a,b\)两点异侧),所以条件是
\(\begin{cases}a<0 \space and\space a<b\\ or\space a>=0\space and \space a>=b\end{cases}\)
(其实就是\(a,b-a\)同号或者说\(a,b\)异号)
对此读者可能有个疑问,为什么不会出现\(a,b\)在原点同一边的情况呢
其实是会的,但是会归为我们刚刚讨论的常函数(因为\(a\)到传送门起点的路上一定经过\(b\))
然后我们来讨论一下坐标
首先想想什么时候\(y=\left | a-b\right |\)
首先如果传送门是从零传送到,那么显然要走完全部的路程
所以第一个斜率改变时的坐标是\((0,\left | a-b\right |)\)
那么什么时候\(y\)值最小呢
显然传送门是从零到\(b\)的,这样子我们就只需要走\(\left | a \right |\)的路程(用于走到传送门起点)
我们得出第二个坐标\((b,\left | a \right |)\)
然后这个函数图像是对称的,我们可以通过前两个推出第三个\((2b,\left | a-b\right |)\)
三,刚刚是经过传送门起点的,那么现在就是相反
读者有兴趣可以自己推,为了节省篇幅,在此直接给出坐标
\((2a,\left | a-b\right |),(b,\left | a \right |),(2b-2a,\left | a-b\right |)\)
知道了坐标,我们需要计算函数的斜率,从而统计\(y\)值
我们可以从最左边开始,如果这段函数的起点\(x\)是\(z\),终点是\(c\),斜率是\(k\),那么\(y\)增加\(k(c-z)\)
我们只要在每个改变的点那里(最坏\(3n\)个),修改\(y\)值,统计最小值就可以了
不过需要注意,初始\(y\)值应该是\(\sum^{n}_{i=1}y[i]\)(这时所有函数是常函数)
由于区间\([-1e8,1e8]\),虽然可以用堆存,但是我还是偷懒用了\(map\)
小技巧:
如果定义了\(map\)的迭代器\(it\),那么\(it->first\)可以返回\(key\),\(it->second\)可以返回\(value\)
\(ACcode:by \space WAduck\)
#include<iostream>
#include<map>
#include<cmath>
#include<cstdlib>
using namespace std;
#define int long long
map<int,int>m;
map<int,int>::iterator it;
int n,sum,ans=999999999,k;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int a,b;
cin>>a>>b;
sum+=llabs(a-b);
if(llabs(b-a)<=llabs(a))continue;
if(a*b>=0)m[2*a]--,m[b]+=2,m[2*b-2*a]--;
else m[0]--,m[b]+=2,m[2*b]--;
}
int last;
for(it=m.begin();it!=m.end();it++)
{
sum+=k*((it->first)-last);
last=it->first;
k+=it->second;
ans=min(ans,sum);
}
cout<<ans;
return 0;
}
D: Promotion Counting
统计了一场\(USACO\)比赛,它们分别统计了赛前和赛后的铜组,银组,金组,白金组的人数
要你求出有分别多少人从铜银金组上升了一个等级
首先我们绝对不会从中间开始考虑,比如金组赛前赛后是\(1 \space \space 1\),那就没有人上分吗
不一定,有可能上来了\(x\)人,上去了\(x\)人
但是换在铜组或者白金组就不一样了
所以我们可以选择从铜组或者白金组开始递推人数
但是最好是白金组
如果我是出题面和造数据的,我就说有人在比赛时刚刚注册了
\(ACcode:by \space kan\_kiz\)
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
int a[7],b[7],ans[7];
int main()
{
for (int i=1;i<=4;i++)
{
scanf("%d%d",&a[i],&b[i]);
}
ans[3]=b[4]-a[4];
ans[2]=b[3]-a[3]+ans[3];
ans[1]=b[2]-a[2]+ans[2];
for (int i=1;i<=3;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
G:Superbull
给定一个集合\(s\)内的\(n\)个数,
进行一种操作:
从集合内选出两个数进行异或(\(xor\)),异或的值计入贡献,并且将两个数中的一个数从集合中删去。
重复\(N-1\)次此操作,直到集合内只剩下最后一个数。
求最大贡献值。
数据范围:\((1<=N<=2000)\)
首先我们不妨猜想一下贪心思路
选前\(N-1\)大的异或和作为答案
很可惜,这是错误的,因为没有考虑不可行情况
什么是不可行情况?
我们假设集合里有\(\{a,b,c,d\}\),\(a \space xor\space b\),\(b \space xor\space c\),\(a \space xor\space c\)是前\(N-1\)大值
我们并不能选这三个组数作为答案
因为如果我们选\(a,b\)删\(a\)
那么\(a,c\)是不可能选到的
反之亦然
也就是说如果删除的某些二元组构成了环,那么这个方法不可行
\(N\)个点没有环又有\(N-1\)条边,显而易见是生成树,为了保证答案最大,只需要连边跑最大生成树即可
我们可以将每个数编号\(1 \to N\),结点两两连一条 边权为【两数异或值】的边
然后对得到的图跑一遍\(Kruskal\)或者\(Prim\)即可。
最后请注意,数据范围较大,需要开\(long\space long\)
思路\(by \space WAduck\) \(ACcode:by\) \(Kan\_kiz\)
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define MAXN 2023
using namespace std;
struct qwq
{
int u,v,w;
}e[4000023];
int w[MAXN];
int tot=0;
int f[MAXN];
int find(int x)
{
if (f[x]==x) return x;
return f[x]=find(f[x]);
}
bool cmp(const qwq x,const qwq y)
{
return x.w>y.w;
}
int main()
{
int n;
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%d",&w[i]);
}
for (int i=1;i<=n;i++)
{
f[i]=i;
}
int xx,yy;
for (int i=1;i<n;i++)
{
for (int j=i+1;j<=n;j++)
{
e[++tot]=(qwq){ i,j,(w[i] xor w[j]) };
}
}
long long ans=0;
sort(e+1,e+tot+1,cmp);
for (int i=1;i<=tot;i++)
{
xx=find(e[i].u); yy=find(e[i].v);
if (xx!=yy)
{
ans+=e[i].w;
f[xx]=yy;
}
}
printf("%lld",ans);
return 0;
}
K:大型聚会
给你一棵\(N\)个节点的树,每次删去一个叶子节点,直到剩下一个节点
但是有\(M\)个限制\(a,b\),表示如果\(a\)没被删除,\(b\)不能删除
最后输出所有能成为最后一个节点的节点
显而易见,对于每个限制,以\(b\)为根时\(a\)的子树(包括自己)都是不可行点
于是我们可以写出一个支持子树修改和换根数据结构,比如\(TopTree\),树链剖分(参见\(LOJ139\))
但是单单这个结论是不够的,我们观察一下这组数据
3 2
1 2
2 3
1 3
3 1
0
0
0
我们的程序会输出
0
1
0
因为没有考虑无解情况
那么如何考虑无解情况呢
该题中的无解就是指没有一个点是可行点,那么我们其实只要找出任何一个可行点就可以了
怎么找?
我们将树上的\(n-1\)条边连成双向边,将限制里的\(a\)连向\(b\)一条单向边
那么,我们每次就只能删除入度为\(1\)的点
因为如果这个点不是叶子节点,入度至少为\(2\),如果有被连有向边(被约束),入度也至少为\(2\)
随便找一个入度为\(1\)的点删除,接下来将它连向的边都删除(也就是说连向的点入度--),然后如果更新后连向的点的入度为\(1\)(不需要每次\(O(n)\)统计,只统计这次删除更新的点就行了),加入待定删除点
最后,如果还没有删除\(n-1\)个点就无法继续删了(没有入度为\(1\)的点了,比如每个点都被互相约束),说明无解,否则有解
这部分代码(为了方便第二种解法,我用了\(vector\)存(\(to\)是双向边,\(d\)是单向边)):
queue<int>q;
int findroot()
{
while(!q.empty())
{
int x=q.front();
q.pop();
if(!deg[x]/*入度*/)return x;
for(int i=0;i<to[x].size();i++)
{
if(--deg[to[x][i]]==1)
{
q.push(to[x][i]);
}
}
for(int i=0;i<d[x].size();i++)
{
if(--deg[d[x][i]]==1)
{
q.push(d[x][i]);
}
}
}
return 0;
}
这样写的话,时间复杂度为\(O(m\space log^2\space m)\)
代码:咕咕咕
但是我们刚刚那个方法显然不够优(\(hyf:\)太暴力了),虽然足以通过本题,但是如果数据加到\(3e6\),显然过不了(吧,有松松松真传我也没办法2333)
首先我们要证(kou)明(hu)两个结论:
一,可行点连通
因为每次修改不可行点只是修改子树,并不破坏可行点的连通性,所以成立
二,任意一个可行点作为根,删除每个限制中的\(a\)的子树,剩下的都是可行点
这个我真不会证,我的想法是将可行点看为树的中间部分,不可行点是树的一些边角,从中间往四周边角扩散不需要考虑从哪里开始,由于可行点连通,所以不需要每次换根
我们将\(a\)的位置打上标记,以\(findroot\)的返回值(任意一个可行点)为起始点,遇到标记直接\(return\),最后输出答案即可
时间复杂度:\(findroot\space O(n)\),\(dfs\space O(n)\)
总时间复杂度\(O(n)\)
\(ACcode: by \space WAduck\)
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
vector<int>to[100001],d[100001];
queue<int>q;
int n,m,deg[100001];
bool vis[100001],ans[100001];
int findroot()
{
while(!q.empty())
{
int x=q.front();
q.pop();
if(!deg[x])return x;
for(int i=0;i<to[x].size();i++)
{
if(--deg[to[x][i]]==1)
{
q.push(to[x][i]);
}
}
for(int i=0;i<d[x].size();i++)
{
if(--deg[d[x][i]]==1)
{
q.push(d[x][i]);
}
}
}
return 0;
}
void dfs(int x,int f)
{
ans[x]=1;
for(int i=0;i<to[x].size();i++)
{
if(!vis[to[x][i]]&&f!=to[x][i])
{
dfs(to[x][i],x);
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
to[x].push_back(y);
to[y].push_back(x);
deg[x]++;
deg[y]++;
}
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
d[x].push_back(y);
deg[y]++;
vis[x]=1;
}
for(int i=1;i<=n;i++)
{
if(deg[i]==1)q.push(i);
}
int root=findroot();
dfs(root,0);
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<endl;
}
return 0;
}
L:请求
初始图上有\(1\)个点编号为\(1\)
接下来有\(N\)个操作
操作\(B(build)\),有一个参数\(x\),表示新增加一个节点(编号为现在图中节点个数),连向(双向边)编号为\(x\)的节点,如果\(x==-1\)则不连边
操作\(Q(query)\),有一个参数\(x\),询问编号为\(x\)的节点到图中相对于\(x\)是最远点的距离(或者说,将\(x\)作为根,询问深度最大的节点的深度)
说道连边,我就想起某\(LCT\)(其实本来是想着玄学做法的\(2333\))
我们可以注意到,如果每个\(build\)操作都去修改全部节点的话,\(build\)操作\(O(n)\),\(query\)操作\(O(1)\)
如果\(build\)不去修改,那每次查询要遍历全图,\(build\)操作\(O(1)\),\(query\)操作\(O(n)\)
我当时想到这个的时候,我还在想能不能写树分块。。。但是显然不好写啊(而且不一定真的能写)
所以我们还是要讨论正解做法
伟大的\(Z\color{red}n\)学长曾经说过:"\(L\)是\(LCT\)维护直径啊","\(sb\)题"
于是我们引入一条重要的结论,树的直径的端点之一一定是树上任意一个点的最远点之一
读者自证不难(我觉得我说出来的太拗口了)
然后我发现我不会\(LCT\)
不过我们思索一下,我们究竟需要维护什么?
维护点之间的距离,插入叶子节点
如果学过树链剖分或者做过其他题,应该知道其实是维护两个点的\(LCA\)
支持动态插入的\(LCA\)算法,其实倍增就行了
倍增\(LCA\)的码风是受了洛谷\(LCA\)第一篇题解感染的
int LCA(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
while(dep[x]>dep[y])
{
x=fa[x][lg[dep[x]-dep[y]]-1];
}
if(x==y)return x;
for(int i=lg[dep[x]]-1;i>=0;i--)
{
if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
int range(int x,int y)
{
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
然后对于每个\(query\),显然答案为\(max(range(x,l/*树的直径左端点*/),range(x,r/*树的直径右端点*/))\)
问题就是如何动态修改树的直径
对于每个\(build\)操作,树的直径只有三种情况
一,不变,\(l=l,r=r\)
二,左端点变为\(x,l=x,r=r\)
三,右端点变为\(x,l=l,r=x\)
其实我们只需要比较刚刚这三种情况的树的直径的大小就好了
然后由于有不连边操作,我们多开一个\(g\)数组记录这个点的在哪颗树上
void link(int x)
{
++idx;//点的编号
if(x==-1)
{
g[idx]=idx;
fa[idx][0]=idx;
l[idx]=r[idx]=idx;
return;
}
fa[idx][0]=x;
dep[idx]=dep[x]+1;
g[idx]=g[x];
for(int i=1;(1<<i)<=dep[idx];i++)
{
fa[idx][i]=fa[fa[idx][i-1]][i-1];
}
int dis1=range(l[g[idx]],r[g[idx]]),dis2=range(idx,l[g[idx]]),dis3=range(idx,r[g[idx]]);
if(dis2>dis1&&dis2>=dis3)r[g[idx]]=idx;
if(dis3>dis1&&dis3>dis2)l[g[idx]]=idx;
}
请注意最后的判断语句不要写成\(dis2>=dis和dis3>=dis2\)或者是\(dis2>dis和dis3>dis2\)(想一想为什么),我觉得其实就我这个傻子这里还写挂了一次
\(ACcode: by \space WAduck\)
#include<iostream>
#include<cmath>
using namespace std;
int n,idx;
int l[100001],r[100001],fa[100001][37],dep[100001],g[100001],lg[100001];
int LCA(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
while(dep[x]>dep[y])
{
x=fa[x][lg[dep[x]-dep[y]]-1];
}
if(x==y)return x;
for(int i=lg[dep[x]]-1;i>=0;i--)
{
if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
int range(int x,int y)
{
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
void link(int x)
{
++idx;
if(x==-1)
{
g[idx]=idx;
fa[idx][0]=idx;
l[idx]=r[idx]=idx;
return;
}
fa[idx][0]=x;
dep[idx]=dep[x]+1;
g[idx]=g[x];
for(int i=1;(1<<i)<=dep[idx];i++)
{
fa[idx][i]=fa[fa[idx][i-1]][i-1];
}
int dis1=range(l[g[idx]],r[g[idx]]),dis2=range(idx,l[g[idx]]),dis3=range(idx,r[g[idx]]);
if(dis2>dis1&&dis2>=dis3)r[g[idx]]=idx;
if(dis3>dis1&&dis3>dis2)l[g[idx]]=idx;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
lg[i]=lg[i-1]+((1<<lg[i-1])==i);
}
for(int i=1;i<=n;i++)
{
char opt;
int x;
cin>>opt>>x;
if(opt=='B')
{
link(x);
}
else
{
cout<<max(range(l[g[x]],x),range(x,r[g[x]]))<<endl;
}
}
return 0;
}