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哦。

posted @ 2021-10-20 10:47  wwlvv  阅读(62)  评论(0)    收藏  举报