10.19骗分大赛
T1 普通快乐
Sol
两种解法。
第一种是我考场上想到的。首先把所有奇葩点推入优先队列中。然后按照dijkstra的写法枚举每个点的入队时序,如果访问到一个点已经访问过,且两次访问来源不是同一奇葩点则最短路径就是两次的距离和。时间复杂度\(O(n\,log\,n)\)。
第二种解法复杂度略高,但能解决单向边的问题。建立一个超级起点和一个超级终点,然后把奇葩点分成两组,一组由起点连长度为0的边,一组向终点连长度为0的边,然后跑起点到终点最短路就是答案。但是可能实际最短的两点被分在同一组。那么问题就转化成了如何用尽量少的次数把每次建图时的点分组,使得每对点都至少一次被分在了不同组里面。抛开随机分组不谈。把每个数二进制拆分,然后按照每一位建图。0、1分开,这样能保证正确性的前提下只跑\(log\,n\)次图。总复杂度\(O(n\ log^2\,n)\)。
有向边在洛谷上有,P5304 [GXOI/GZOI2019]旅行者
第一种解法不能解有向图是因为每次它都是从奇葩点出发,可能无法形成有效路径。而解决方案是先正着跑一次,记录每个点最短和次短,然后建反图同理,最后的结果就是每个点最短和的最值,如果两个最短来源相同就比较次短。
Code(解法一)
#include<bits/stdc++.h>
using namespace std;
const int maxn=200010;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=0;c=getchar();}
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c&15),c=getchar();
return f?x:-x;
}
struct edge
{
int to,next,v;
}e[maxn<<1];
int h[maxn],ei;
inline void add(int x,int y,int v)
{
e[++ei]=(edge){y,h[x],v};
h[x]=ei;return;
}
int n,m,k;
int dis[maxn],f[maxn],ans[maxn];
bool vis[maxn];
struct node
{
int id,v,fr;
bool operator<(const node &x)const
{
return v>x.v;
}
};
priority_queue<node>qu;
int main()
{
freopen("path.in","r",stdin);
freopen("path.out","w",stdout);
n=read();m=read();k=read();
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),z=read();
add(x,y,z);add(y,x,z);
}
memset(dis,-1,sizeof(dis));
for(int i=1;i<=k;i++)
{
int x=read();
qu.push((node){x,0,x});
}
while(!qu.empty())
{
node x=qu.top();qu.pop();
if(~dis[x.id])
{
if(x.fr==f[x.id])continue;
return printf("%d\n",dis[x.id]+x.v)&0;
}
dis[x.id]=x.v;f[x.id]=x.fr;
for(int i=h[x.id];i;i=e[i].next)
{
int to=e[i].to;
qu.push((node){to,dis[x.id]+e[i].v,f[x.id]});
}
}
return 0;
}
(解法一对于有向图,代码是可爱的漠寒提供的)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxm=5e5+5;
const int maxn=100005;
struct hp
{
int next,to,w;
}edge[maxm<<1];
struct hpp
{
int from,pos,w;
bool operator > (const hpp tmp) const
{
return w>tmp.w;
}
};
int qi[maxn];
int tot=0,head[maxn];
void add_edge(int u,int v,int w)
{
tot++;
edge[tot].next=head[u];
edge[tot].w=w;
edge[tot].to=v;
head[u]=tot;
}
priority_queue<hpp,vector<hpp>,greater<hpp> > q;
int n,m,k;
int f[maxn][3];
int g[maxn][3];
int qq[maxn];
void dijistra()
{
memset(f,0x3f,sizeof(f));
memset(g,0,sizeof(g));
for(int i=1;i<=k;i++)
{
hpp a;
a.from=qi[i];a.w=0;a.pos=qi[i];
q.push(a);
g[qi[i]][1]=qi[i];
f[qi[i]][1]=0;
}
while(!q.empty())
{
hpp x=q.top();
q.pop();
int u=x.pos;
//cout<<u<<" "<<f[u][1]<<" "<<g[u][1]<<" "<<f[u][2]<<" "<<g[u][2]<<endl;
//cout<<x.pos<<" "<<x.from<<endl;
if(qq[x.pos]&&(x.from!=x.pos))
{
printf("%lld\n",x.w);
while(!q.empty())
{
hpp y=q.top();
q.pop();
}
return;
}
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(f[v][1]>x.w+edge[i].w){
if(g[v][1]!=x.from){
g[v][2]=g[v][1];
f[v][2]=f[v][1];
}
g[v][1]=x.from;
f[v][1]=x.w+edge[i].w;
hpp b;
b.pos=v;
b.w=x.w+edge[i].w;
b.from=x.from;
q.push(b);
}
else if(g[v][1]!=x.from&&f[v][2]>x.w+edge[i].w){
g[v][2]=x.from;
f[v][2]=x.w+edge[i].w;
hpp b;
b.pos=v;
b.w=x.w+edge[i].w;
b.from=x.from;
q.push(b);
}
}
}
}
signed main()
{
//freopen("path.in","r",stdin);
//freopen("path.out","w",stdout);
int t;
scanf("%d",&t);
while(t--)
{
memset(head,0,sizeof(head));
memset(qq,0,sizeof(qq));
tot=0;
scanf("%lld %lld %lld",&n,&m,&k);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld %lld %lld",&u,&v,&w);
add_edge(u,v,w);
}
for(int i=1;i<=k;i++)
{
scanf("%lld",&qi[i]);
qq[qi[i]]=1;
}
dijistra();
}
return 0;
}
T2 小王子
Sol
这题题干写一大堆,结果是\(lyd\)书上原题。甚至源码直接过的那种……
有一个性质:如果一条白边在两个或更多环上,那删掉它没有任何意义;只在一个环上,那么删它对答案贡献为\(1\);不在任何环上,对答案贡献为\(m\)。证明显然。每一条黑边\((x,y)\)的实际意义就是使原来的树上\((x,y)\)路径中所有边所在环数\(+1\)。
于是原题目就变成了:给定一棵树,每次把两点\(x,y\)间路径上所有边权值\(+1\),求最后有几个点权值为0,几个点权值为1。
考场上感觉这题一看就是树剖,所以想都没想就开始敲。但数据结构长期以来就是我最鸡肋的东西,树剖写的可以说是现推一次原理写的。最后果不其然,挂成20pts。
回去翻了书才知道,由于这个题的询问只在最后有一次,所以可以直接树上差分,最后遍历求解即可。码量小,常数还更优……
树上差分做法就很显然了:首先假定每个点表示它与父亲的连边。每次修改在\(x,y\)上打一个\(+1\)标记,在\(LCA(x,y)\)上打标记\(-2\)。最后\(dfs\)求子树和确定当前点实际值计算即可。
Code(树剖做法被我删了)
#include<bits/stdc++.h>
using namespace std;
const int maxn=200010;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=0;c=getchar();}
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c&15),c=getchar();
return f?x:-x;
}
struct edge
{
int to,next;
}e[maxn<<1];
int h[maxn],ei;
inline void add(int x,int y)
{
e[++ei]=(edge){y,h[x]};
h[x]=ei;return;
}
int n,m,dep[maxn],fa[maxn],xb[maxn][20];
int sum[maxn];
inline void dfs(int x,int f)
{
for(int i=h[x];i;i=e[i].next)
{
int to=e[i].to;
if(to==f)continue;
dep[to]=dep[x]+1;fa[to]=x;
dfs(to,x);
}
return;
}
inline void pre()
{
for(int i=1;i<=n;i++)xb[i][0]=fa[i];
for(int j=1;j<=18;j++)
{
for(int i=1;i<=n;i++)xb[i][j]=xb[xb[i][j-1]][j-1];
}
return;
}
inline int LCA(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
for(int i=18;i>=0;i--)
{
if(dep[xb[x][i]]>=dep[y])x=xb[x][i];
}
if(x==y)return x;
for(int i=18;i>=0;i--)
{
if(xb[x][i]!=xb[y][i])
{
x=xb[x][i];y=xb[y][i];
}
}
return fa[x];
}
int main()
{
freopen("astronaut.in","r",stdin);
freopen("astronaut.out","w",stdout);
n=read();m=read();
for(int i=1;i<n;i++)
{
int x=read(),y=read();
add(x,y);add(y,x);
}
dep[1]=1;dfs(1,0);
pre();
for(int i=1;i<=m;i++)
{
int x=read(),y=read();
sum[x]++;sum[y]++;sum[LCA(x,y)]-=2;
}
int ans=0;
for(int i=2;i<=n;i++)
{
if(sum[i]==0)ans+=m;
if(sum[i]==1)ans++;
}
printf("%d\n",ans);
}
T3 简单的打击
Sol
总算不是一道贺的题了(指出题人)。
50分很好想:用桶来计每个数值出现次数,然后枚举众数值取最大即可。实际运行不卡常能有95,平均75,数据很水。
如果用循环展开优化,直接能卡过。显然这是不合适的,于是wwlw就重造了数据,才发现原来的造数据程序直接用\(rand()%p\),也就是说它造出来的数据根本跑不满。然后循环展开就\(TLE\)爆了。
正解是卷积+FFT/NTT,但是我显然不会,所以我直接跑的\(SA\)算法给它糊过去了。其实锅很多,不过数据太水。
\(SA\),也就是模拟退火算法,用于在你做不出来正解的时候求多峰函数的最值。注意:对于任意函数都可以,但是峰越少就越容易得到正确答案。
模拟退火的思路如下:
首先选取一个初值,一般是\(\frac{l+r}{2}\),然后计算得到这个值对应函数值。
设置一个初始温度\(temp\)和每次退火衰减率\(step\),表示每次随机的范围。
每次\(rand\)一个长度值\(dis\),然后分别求当前最值的向左/向右求出\(x-dis、x+dis\)的值,然后更新最值和位置。
由于随机范围越来越小,所以最后一定是在函数的一个波峰上更新。但是这是不能保证正确性的,因为很可能远处还有更高峰,但是随机种子并没有到达那里,所以需要重新做几次退火算法,重新从最远的地方开始随机,这样多做几次就能够尽最大可能地移到最高峰上面。每多做一次\(SA\),其答案的正确性就要更大一些。所以可以套上一个计时函数\(clock\),只要当前运行时间比限制时间小大约\(100ms\),就一直跑\(SA\)。这样做就可以保证不超时的情况下效率更高。
Code(当然是\(SA\)啦)
#include<bits/stdc++.h>
using namespace std;
const int maxn=200010;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=0;c=getchar();}
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c&15),c=getchar();
return f?x:-x;
}
int ta[maxn],tb[maxn],n;
int rst[maxn];
int mx=0,mx1=0,ans,res;
inline int f(int x)
{
if(rst[x])return rst[x];
int cnt=0;
for(int i=1;i<x;i++)
{
cnt+=min(ta[i],tb[x-i]);
}
return rst[x]=cnt;
}
inline void sa()
{
double temp=100.0,step=0.98;
int len=(mx+mx1),l=1,r=(mx+mx1);
while(temp>=0.04)
{
int dis=1.0*len*temp*(rand()%100+1)/10000;
int l1=ans-dis;
if(l1>=l)
{
int tmp=f(l1);
if(tmp>res)
{
res=tmp;
ans=l1;
}
}
int l2=ans+dis;
if(l2<=r)
{
int tmp=f(l2);
if(tmp>res)
{
res=tmp;
ans=l2;
}
}
temp*=step;
}
return;
}
int main()
{
// freopen("hit.in","r",stdin);
// freopen("hit.out","w",stdout);
clock_t beg=clock();
srand(time(NULL));
n=read();
for(int i=1;i<=n;i++)
{
int x=read();ta[x]++;
mx=max(mx,x);
}
for(int i=1;i<=n;i++)
{
int x=read();tb[x]++;
mx1=max(mx1,x);
}
ans=(mx+mx1+1)>>1;
res=f(ans);
while(clock()-beg<1000)sa();
printf("%d\n",res);
}
T4 WC
Sol
标算是树状数组/平衡树套cdq分治,来维护一个由四维偏序优化成的三维偏序,显然我不会。
好处是这个题暴力有60pts。因为暴力修改操作是\(O(1)\)的,而查询操作是\(O(修改次数)\)的,也就是说在最坏情况下复杂度也是\(O(\frac{n^2}{4})\)的,常数又很小,所以能过\(n\leq 20000\)的点。不过数据似乎又水过头了,能直接得70。
Code
暴力还粘什么Code哦。

浙公网安备 33010602011771号