NOIP2017day2题解

时隔这么久真心的是忘了写noip系列的题解了

刚好今天复习的时候重新做了一遍17年day2的题,所以就写一篇题解

T1

奶酪

题目描述

现有一块大奶酪,它的高度为 hh,它的长度和宽度我们可以认为是无限大的,奶酪 中间有许多 半径相同 的球形空洞。我们可以在这块奶酪中建立空间坐标系,在坐标系中, 奶酪的下表面为z = 0z=0,奶酪的上表面为z = hz=h。

现在,奶酪的下表面有一只小老鼠 Jerry,它知道奶酪中所有空洞的球心所在的坐 标。如果两个空洞相切或是相交,则 Jerry 可以从其中一个空洞跑到另一个空洞,特别 地,如果一个空洞与下表面相切或是相交,Jerry 则可以从奶酪下表面跑进空洞;如果 一个空洞与上表面相切或是相交,Jerry 则可以从空洞跑到奶酪上表面。

位于奶酪下表面的 Jerry 想知道,在 不破坏奶酪 的情况下,能否利用已有的空洞跑 到奶酪的上表面去?

空间内两点距离公式

dist(P1,P2)=sqrt((x1-x2)^2+(y1-y2)^2+(z1-z2)^2))

输入输出格式

输入格式:

每个输入文件包含多组数据。

的第一行,包含一个正整数 TT,代表该输入文件中所含的数据组数。

接下来是 T 组数据,每组数据的格式如下: 第一行包含三个正整数 n,,h 和 r,两个数之间以一个空格分开,分别代表奶酪中空 洞的数量,奶酪的高度和空洞的半径。

接下来的 n 行,每行包含三个整数 x,y,z,两个数之间以一个空格分开,表示空 洞球心坐标为(x,y,z)。

输出格式:

T 行,分别对应 T 组数据的答案,如果在第 i 组数据中,Jerry 能从下 表面跑到上表面,则输出Yes,如果不能,则输出No 

 真的是很水的一道题,day2的T1比day1的T1简单太

我们可以很轻松地发现点的数量很少,于是我们可以考虑将小白鼠所在的平面设定为0点,然后用n^2的时间枚举两两点对之间的距离,用其判断是否能够在它们之间连边(注意这道题好像卡了精度,所以个人建议最好不要直接开根号,可以把两边都平方然后用1ll转换变量类型,这样就可以避免精度的问题)

连边之后从0点开始跑BFS,当任意一个节点的坐标的高点加上半径(其所能够达到的最大高度)比h要大的时候就输出YES,如果所有的点都不行的话就输出NO,那么BFS时间复杂度O(n),建图的复杂度O(n^2),于是愉快地切掉了

丑陋的代码

#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
const int MAXN=1e3+5;
int n,h,r;
struct Node 
{
    int x;
    int y;
    int z;
    int hmax;
}node[MAXN];
struct Edge
{
    int nxt;
    int to;
}edge[MAXN<<3];
int num;
int head[MAXN];
void add(int from,int to)
{
    edge[++num].nxt=head[from];
    edge[num].to=to;
    head[from]=num;
}
long long dist(int x1,int y1,int z1,int x2,int y2,int z2)
{
    return ((long long)(x1-x2)*(x1-x2)+(long long)(y1-y2)*(y1-y2)+(long long)(z1-z2)*(z1-z2));
}
bool vis[MAXN];
std::queue<int>q;
int main()
{
    //std::freopen("cheese.in","r",stdin);
    //std::freopen("cheese.out","w",stdout);
    int T;
    std::scanf("%d",&T);
    while(T--)
    {
        std::memset(edge,0,sizeof(edge));
        std::memset(head,0,sizeof(head));
        std::memset(node,0,sizeof(node));
        std::memset(vis,0,sizeof(vis));
        while(!q.empty()) q.pop();
        num=0;
        std::scanf("%d%d%d",&n,&h,&r);
        for(int i=1;i<=n;i++)
        {
            std::scanf("%d%d%d",&node[i].x,&node[i].y,&node[i].z);
            node[i].hmax=node[i].z+r;
            if(node[i].z<=r)
            {
                add(0,i);
                add(i,0);
            }
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(i==j) continue;
                if(dist(node[i].x,node[i].y,node[i].z,node[j].x,node[j].y,node[j].z)<=((long long)2*r*2*r))
                {
                    add(i,j);
                }
            }
        }
        q.push(0);
        vis[0]=1;
        bool flag=1;
        while(!q.empty())
        {
            bool bb=0;
            int u=q.front();
            q.pop();
            for(int i=head[u];i;i=edge[i].nxt)
            {
                int v=edge[i].to;
                if(!vis[v])
                {
                    if(node[v].hmax>=h)
                    {
                        std::printf("Yes\n");
                        bb=1;
                        flag=0;
                        break;
                    }
                    vis[v]=1;
                    q.push(v);
                }
            }
            if(bb==1)
            {
                break;
            }
        }
        if(flag) std::printf("No\n");
    }
    return 0;
}
/*
3
2 4 1
0 0 1
0 0 3
2 5 1
0 0 1
0 0 4
2 5 2
0 0 2
2 0 4
*/

T2

宝藏

题目描述

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 nn 个深埋在地下的宝藏屋, 也给出了这 nn 个宝藏屋之间可供开发的mm 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。

新开发一条道路的代价是:

L*K

L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值。

输入输出格式

输入格式:

第一行两个用空格分离的正整数 n,mn,m,代表宝藏屋的个数和道路数。

接下来 m 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏 屋的编号(编号为 1−n),和这条道路的长度 v。

输出格式:

一个正整数,表示最小的总代价。

 

首先我可以明确表示prim是错误的.........但是它可以很好的作为骗分算法(由于官方数据比较水,所以很多的暴力和骗分能够苟住很多分,据说有模拟退火+primA掉的(不过我估计调了很久的参))

我们可以从数据上面一眼看出是状压dp

然后我们直接考虑枚举根节点然后dfs,用二进制表示状态稍微剪一剪枝就好了

贴上个人今天下午的时候无聊写的代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define lowbit(s) s&-s
const int MAXN=15;
const int INF=1e9+7;
int a[MAXN];
int b[MAXN];
int G[MAXN][MAXN];
int dp[(1<<MAXN)+1];
int lo[MAXN];
int dis[MAXN];
int n,m;
void dfs(int s)
{
    int S=s;
    for(int i=1;i<=n;i++) 
    {
        if(s&(1<<i-1))
        for(int j=1;j<=n;j++)
        {
            if(!(s&(1<<j-1)))
            {
                if(G[i][j]!=G[0][0]&&G[i][j]*dis[i]+dp[s]<dp[s|(1<<j-1)])
                {
                    int tmp=dis[j];
                    dis[j]=dis[i]+1;
                    dp[s|(1<<j-1)]=dis[i]*G[i][j]+dp[s];
                    dfs(s|(1<<j-1));
                    dis[j]=tmp;
                }
            }
        }
    }
}
int main()
{
    std::scanf("%d%d",&n,&m);
    for(int i=1;i<MAXN;i++)
    {
        lo[i]=1<<i;
    }
    lo[0]=1;
    std::memset(G,63,sizeof(G));
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        std::scanf("%d%d%d",&u,&v,&w);
        G[u][v]=G[v][u]=std::min(G[u][v],w);
    }
    int ans=INF;
    for(int root=1;root<=n;root++)
    {
        std::memset(dis,63,sizeof(dis));
        std::memset(dp,63,sizeof(dp));
        dp[(1<<root-1)]=0;
        dis[root]=1;
        dfs(1<<root-1);
        ans=std::min(ans,dp[(1<<n)-1]);
    }
    std::printf("%d\n",ans);
    return 0;
}

这样的复杂度卡在正解边上(我估计可以造特殊数据卡掉),不过我之前在考场上(今年4月时候的模拟考)有写过一条正解的代码(考场上调了贼久终于写对了,结果T1挂了23333)

定义dp[s][k]为 已经选择的状态为S的点,而且最深的点深度为k,然后枚举新加入的点和已经有的点然后用深度乘上去计算花费就可以转移,其实要比dfs复杂很多,而且dfs枚举根节点(n^3*2^n*log(n))复杂度也过的去,总而言之我更喜欢第一种写法,( 然而考场上并没有思考简单的做法,对着那个深度dp就是一顿乱搞,调了2.5h终于搞出来了.....)

第二种方法的代码(考场的SB代码...当时的码风好丑来着)

// luogu-judger-enable-o2
#include<cstdio>
#include<cstring>
#include<algorithm>
const int MAXN=(1<<13)-1;
const int INF=1000005;
int f[15][MAXN];
int map[105][105];
int g[MAXN];
int v[15];
int p[15];
int lo[MAXN];
int s[MAXN];
int lowbit(int x)
{
    return x&-x;
}
int tot;
int main()
{
    //std::freopen("treasure.in","r",stdin);
    //std::freopen("treasure.out","w",stdout);
    int n,m;
    std::scanf("%d%d",&n,&m);
    for(int i=0;i<=n-1;i++) 
    {
        for(int j=0;j<=n-1;j++) 
        {
            map[i][j]=INF;
        }
    }
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        std::scanf("%d%d%d",&u,&v,&w);
        map[--u][--v]=map[v][u]=std::min(map[u][v],w);
    }
    for(int i=0;i<=n-1;i++)
    {
        lo[1<<i]=i;
    }
    memset(f,63,sizeof(f));
    for(int i=0;i<=n-1;i++)
    {
        f[0][1<<i]=0;
    }
    for(int i=0;i<=n-1;i++)
    {
        for(int x=0;x<=(1<<n)-1;x++)
        {
            tot=0;
            for(int k=0;k<=n-1;k++)
            if(!(x&(1<<k)))
            {
                v[tot]=INF;
                p[tot]=1<<k;
                for(int j=x;j;j-=lowbit(j))
                {
                    int l=lo[lowbit(j)];
                    v[tot]=std::min(v[tot],map[k][l]*(i+1));
                }
                tot++;
            }
            for(int j=1;j<=(1<<tot)-1;j++)
            {
                g[j]=g[j-lowbit(j)]+v[lo[lowbit(j)]];
                s[j]=s[j-lowbit(j)]|p[lo[lowbit(j)]];
                f[i+1][x|s[j]]=std::min(f[i][x]+g[j],f[i+1][x|s[j]]);
            }
        }
    }
    int ans=1<<30;
    for(int i=0;i<=n-1;i++) 
    {
        ans=std::min(ans,f[i][(1<<n)-1]);
    }
    std::printf("%d\n",ans);
    return 0;
}
/*
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1
 
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2
*/

T3 列队

题目描述

Sylvia 是一个热爱学习的女孩子。

前段时间,Sylvia 参加了学校的军训。众所周知,军训的时候需要站方阵。

Sylvia 所在的方阵中有n名学生,方阵的行数为 n,列数为 m。

为了便于管理,教官在训练开始时,按照从前到后,从左到右的顺序给方阵中 的学生从 1 到 n \times mn×m 编上了号码(参见后面的样例)。即:初始时,第 ii 行第 jj 列 的学生的编号是(i-1)\* m + j

然而在练习方阵的时候,经常会有学生因为各种各样的事情需要离队。在一天 中,一共发生了 qq件这样的离队事件。每一次离队事件可以用数对(x,y) (1≤x≤n,1≤y≤m)描述,表示第 x 行第 y 列的学生离队。

在有学生离队后,队伍中出现了一个空位。为了队伍的整齐,教官会依次下达 这样的两条指令:

  1. 向左看齐。这时第一列保持不动,所有学生向左填补空缺。不难发现在这条 指令之后,空位在第 x 行第 m列。

  2. 向前看齐。这时第一行保持不动,所有学生向前填补空缺。不难发现在这条 指令之后,空位在第 n 行第 m列。

教官规定不能有两个或更多学生同时离队。即在前一个离队的学生归队之后, 下一个学生才能离队。因此在每一个离队的学生要归队时,队伍中有且仅有第 n 行 第 m 列一个空位,这时这个学生会自然地填补到这个位置。

因为站方阵真的很无聊,所以 Sylvia 想要计算每一次离队事件中,离队的同学 的编号是多少。

注意:每一个同学的编号不会随着离队事件的发生而改变,在发生离队事件后 方阵中同学的编号可能是乱序的。

输入输出格式

输入格式:

输入共q+1 行。

第 1 行包含 3 个用空格分隔的正整数 n,m,q,表示方阵大小是 n 行 m 列,一共发 生了 q 次事件。

接下来 q 行按照事件发生顺序描述了 q 件事件。每一行是两个整数 x,y,用一个空 格分隔,表示这个离队事件中离队的学生当时排在第 x 行第 y 列。

输出格式:

按照事件输入的顺序,每一个事件输出一行一个整数,表示这个离队事件中离队学 生的编号。

我们发现我们其实需要维护的东西并不是很复杂,对于每一行其实都只有两部分组成,一部分是最后一列之前的人,另一部分是最后一列上的人,每一次出队的步骤可以大致拆分成为如下的部分:某一行上的某个人出队,下一行最后一列上的那个人进入这一行,这个人进入最后一行最后一列

然后我们要询问当前离队的人的编号

那么我们可以考虑使用一些数据结构来维护这个操作

我们可以发现数据范围的3e5是非常具有"log(n)"感觉的数据,再加上我们要处理的是很多个线性的区间问题,所以说我们可以很轻松地想到线段树、树状数组、Splay等一些列数据结构(而且这三个都是能A掉这道题的........)

个人采用的是线段树的做法,Splay的做法个人并不推荐,因为它的常数很大而且截至目前为止,它还没有进入noip的考试范围,属于超纲内容。再者,用我们某位学长的原话:“你用这道题目不要求的复杂数据结构A掉这道题目,只能说明你的水平不足以支持你用正常的方法A掉它,出题人大概会很看不起你把.....”

我们考虑建立n+1颗线段树,对于n行来说,我们的每一行都建立一颗线段树以维护其1~m-1列上的部分信息(为什么是部分我下文会提到),注意是1~m-1列,这样我们就有了n颗线段树,然后,然后,我们再将最后一列,第m列建成一颗线段树,这样就有了n+1颗线段树(空间问题待会儿再谈)

我们思考一下为什么这么做,因为我们每一次的修改涉及两个方面,一个方面是某一行上的某个点,这个是根据不同行而不同的, 另一个则是最后一列的某个点,这个是完全固定的,所以说我们每一次的修改操作就相当于是在某一行与最后一列上进行,这样的建立线段树的方法就能够保证我们的所有状态完全不会漏

然而,如果我们们要用线段树直接来维护,我们会发现一个严重的问题:woc线段树不支持插入与删除操作(Splay支持,所以说很多人都是用Splay写的..............)

所以说我们用线段树来维护每一行某个区间内相对原来区间来说已经有多少个人已经离队了,为什么要这么做呢?因为我们可以发现离队之前人的编号是固定的而且在一个定的区间以内,所以记录下删除的人数以后就可以用区间总人数减去删除的人数与当前要查询的位置作比较,然后二分得出这个位置的人到底是一开始就在队列里面还是因为移动队列而加入的。

这样的话,我们就完成了删除操作,即通过记录删除的人的人数获得其某个位置到底是原区间上的还是新加入的。

然后我们要由这个位置得到这次查询的答案。那么该怎么做呢?

这个就涉及到插入操作了。

我们再建立n+1个vector不定长数组,分别表示每一个线段树上插入的新节点的编号(因为线段树不支持插入操作)

然后我们可以考虑当前位置

我们首先可以发现,如果这个位置的人是原来区间上就有的,那么我们可以马上计算出其编号。

否则的话,我们就用它的排名减去现在区间上的数的个数,然后在vector里面可以找到其位置

然后我们每一次删除节点(在线段树上加其被删除的树的数量),并且在vector中push进入新加入的节点,并且一直维护这种操作就好了。

至于空间问题,我们发现需要的节点只跟询问数量有关

所以我们动态开节点就好了..............

至此noip2017day2 300pts get!

(我估计代码比我的讲述好理解23333)

// luogu-judger-enable-o2
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define OP "phalanx"
#define LL long long
const int MAXN=3e5+5;
int n,m,q;
int lim;
class Node
{
    public:
        int ls;
        int rs;
        int sum;
        void update(void);
}node[MAXN*20];
int tot;
int root[MAXN+1];
void Node::update()
{
    sum=node[ls].sum+node[rs].sum;
}
int newnode()
{
    tot++;
    node[tot].ls=node[tot].rs=0;
    node[tot].sum=0;
    return tot; 
}
std::vector<LL>vc[MAXN+1];
void modify(int &nd,int l,int r,const int pos)
{
    if(!nd)
    {
        nd=newnode();
    }
    if(l==r)
    {
        node[nd].sum++;
        return;
    }
    int mid=l+r>>1;
    node[nd].sum++;
    if(pos<=mid)
    {
        modify(node[nd].ls,l,mid,pos);
    }
    else
    {
        modify(node[nd].rs,mid+1,r,pos);
    }
    node[nd].update();
}
int query(int nd,int l,int r,int pos)
{
    if(l==r)
    {
        return l;
    }
    int mid=l+r>>1;
    int lz=mid-l+1-node[node[nd].ls].sum;
    if(pos<=lz)
    {
        return query(node[nd].ls,l,mid,pos);
    }
    else 
    {
        return query(node[nd].rs,mid+1,r,pos-lz);
    }
}
LL del1(int x,LL y)
{
    int pos=query(root[n+1],1,lim,x);
    modify(root[n+1],1,lim,pos);
    LL ans=pos<=n?1ll*pos*m:vc[n+1][pos-n-1];
    vc[n+1].push_back(y?y:ans);
    return ans;
}
LL del2(int x,int y)
{
    int pos=query(root[x],1,lim,y);
    modify(root[x],1,lim,pos);
    LL ans=pos<m?1ll*(x-1)*m+pos:vc[x][pos-m];
    vc[x].push_back(del1(x,ans));
    return ans;
}
int main()
{
    //std::freopen("testdata.in","r",stdin);
    //std::freopen("testdata.txt","w",stdout); 
    std::scanf("%d%d%d",&n,&m,&q);
    lim=std::max(n,m)+q;
    while(q--)
    {
        int x,y;
        std::scanf("%d%d",&x,&y); 
        if(y==m)
        {
            std::printf("%lld\n",del1(x,0));
        }
        else
        {
            std::printf("%lld\n",del2(x,y));
        }
    }
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值