NOIP2018 解题报告

NOIP2018 解题报告

前记

在本届noip,作为第一年参加提高组的我,感受到了各位大佬神仙恐怖如斯的实力。身在弱省,但是依旧难以取得成绩,果然oi赛场,菜是原罪

好了,到了赛后,还是总结一下题目,重整旗鼓才是

D1T1 铺设道路

题目:

春春是一名道路工程师,负责铺设一条长度为 n 的道路。

铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n 块首尾相连的区域,一开始,第 i 块区域下陷的深度为 d[i].

春春每天可以选择一段连续区间 [L,R] ,填充这段区间中的每块区域,让其下陷深度减少 1。在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0 。

春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0 .

好好说话:

就是说给你一个数列,a1,a2...an, 要求每一次操作可以使一段连续的数字减少1,询问到所有数字变成0,最少需要操作多少次?

解题:

初看到题,不禁窃喜一阵,“嘿,今年noip肯定不会爆零了”。因为这道题竟然是2013年的noip原题“积木大赛”的平行世界,而且清清楚楚的记得是在和高二训练时,训练赛上一道我当时就已经ac了的题。

思路非常简单,甚至谈不上是贪心:假如对于当前状态存在一段连续且极大的正整数(从l到r),那么这次操作一定就是将l到r的所有数字减少1,操作次数+1.

然后,运用分治的思想,每次从头到尾找到一段连续的正整数,即可以最优策略的一步。处理之后,递归处理两侧即可。细节也比较简单。

震惊,洛谷竟然直接将铺设道路的标签加上了“2013年noip”

代码如下:

#include <cstdio>

const int MAX=1e5+5;
int n,ans;
int road[MAX];

void finds(int,int);

int main(){
    //freopen("test.in","r",stdin);
    //freopen("test.out","w",stdout);

    scanf("%d",&n);
    for(int i=1;i<=n;++i) scanf("%d",&road[i]);

    finds(1,n);
    printf("%d",ans);

    return 0;
}

void finds(int l,int r){
    if(l>r) return;
    int rec,mid=0x3f3f3f3f;
    for(int i=l;i<=r;++i){
        if(road[i]<mid){
            mid=road[i];
            rec=i;
        }
    }
    for(int i=l;i<=r;++i) road[i]-=mid;
    ans+=mid;
    finds(l,rec-1); finds(rec+1,r);
}

D1T2 货币系统

题目:

在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为 n、面额数组为 a[1..n] 的货币系统记作 (n,a)。

在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i] 满足 a[i]×t[i] 的和为 x。然而, 在网友的国度中,货币系统可能是不完善的,即可能存在金额 x 不能被该货币系统表示出。例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。

两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。

现在网友们打算简化一下货币系统。他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。他们希望你来协助完成这个艰巨的任务:找到最小的 m

好好说话:

给你一些正整数,要求若一个数字可以由数字乘以特定的系数表示出来(\(m=\sum_{i=1}^{all num} k_i\times a_i\)),那么这个数字就应该被剔除。求剩下的数字个数。

题解:

第一眼看,有点执着于“唯一分解定理”

但是,其实这道题只要推一推样例就会恍然大悟。一推样例,就会猛地发现,不论给出什么样的数据,正整数中最小的那一个一定无法被代替。接着,便可以联想到,将这些正整数排序,越小的数字越不容易被别的数字替代,越大的数字越容易被替代。

至此,这道题的贪心思路已经出来了。我们只需要排一遍序,从小到大用背包判断每一个数能否被前面的数字替代即可

然而,考场上我竟然就是忘记了背包,竟然用了dfs暴力遍历所有种可能性?果然士别三日,就更能发觉三日前的沙雕

代码如下:

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAX=105;
int t,n;
int coin[MAX];
bool vis[25005];

int read();

int main(){
    //freopen("test.in","r",stdin);
    //freopen("test.out","w",stdout);

    t=read();
    for(;t;--t){
        n=read(); for(int i=1;i<=n;++i) coin[i]=read();
        sort(coin+1,coin+1+n);
        memset(vis,false,sizeof(vis));
        vis[0]=true;
        int ans=0;
        for(int i=1;i<=n;++i){
            if(vis[coin[i]]==false){
                ans++;
                for(int j=0;j<=25000-coin[i];++j){
                    if(vis[j]) vis[j+coin[i]]=true;
                }
            }
        }
        printf("%d\n",ans);
    }

    return 0;
}

int read(){
    char tmp=getchar(); int sum=0;
    while(tmp<'0'||tmp>'9') tmp=getchar();
    while(tmp>='0'&&tmp<='9'){
        sum=sum*10+tmp-'0';
        tmp=getchar();
    }
    return sum;
}

D1T3 旅行

题目如下:

小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。

小Y了解到, X国的 n 个城市之间有 m 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。

小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可 以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到上一个城市。当小 Y 回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。

为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 n 的序列。她希望这个序列的字典序 最小,你能帮帮她吗? 对于两个长度均为 n 的序列 A 和 B,当且仅当存在一个正整数 x,满足以下条件时, 我们说序列 A 的字典序小于 B。

·对于任意正整数 1≤i<x,序列 A 的第 i 个元素 \(a_i\)和序列 B 的第 i 个元素 \(b_i\) 相同。

·序列 A 的第 x 个元素的值小于序列 B 的第 x 个元素的值。

好好说话:

给出一个树或者是基环树,让你找出最小的dfs序。对于基环树,则可以进行“反悔”操作,即可以从环的左右两边分别走,而不必必须从某一边入,然后完整的走一个环

题解:

还原赛场思路。“哦,60%是树。嗯,dfs一遍就ok”,心中已是窃喜不已。

接着,“嗯?为什么剩下的数据在树的基础上格外给了一条边?那是什么?”......于是,深陷沉思中,再也得出什么有用的结论

现在,我已经知道了,树上加上一条边即构成了基环树,那么该怎么解决呢。

考虑如下的事实:每走一条没有访问过的边,必定会走向一个没有访问过的点(嗯,基环树上好像并不是如此,但是接着看下去),所以想要n个点的dfs序,最少只需要走n-1条为访问过的边,而在这些由走n-1条推出的答案中,必定含有本数据的最优解。(显然嘛,最优解怎么会没有事,到处闲逛,它不会再由为访问过的边访问一次已访问过的点,这么做没意义)

所以我们可以暴力断边,每次都枚举断的那一条边,然后跑一遍dfs即可

但是!这个\(n^2\)的算法并不是最优秀的。还存在一个更厉害的\(n \log n\)的算法:

考虑一下什么时候我们可以反悔?

1.一定是在环上,否则子树的点将永远再也访问不到

2.回溯后的点,一定要小于由当前的访问到的最小的点的点权。否则回溯过去之后,dfs序并不会得到优化。

嗯,主要思路就这么多。模拟一下思路,用代码展示出来,就是这道题的另外一种解法。

补充一点,需要提前跑一遍dfs,判断出环的根节点rt,在环上且紧邻rt的两个节点中,较小的u1和较大的u2

代码如下:

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
#include <ctime>
using namespace std;

const int MAX=5e3+5;
int n,m;
int ecnt,edge[MAX<<1],head[MAX],nxt[MAX<<1],vis[MAX];
int ban[MAX<<1],cur;
int ans[MAX],vis1[MAX],vis2[MAX];
int rt,u1,u2;
clock_t s,e;

vector <int> edg[MAX];

int read();
void insert(int,int,int);
void dfs1(int,int);
void dfs2(int,int);
int getnxt(int,int);
void dfs3(int,int);
void dfs4(int,int,int);

int read();

int main(){
    //freopen("test.in","r",stdin);
    //freopen("test.out","w",stdout);

    n=read(); m=read();

    for(register int i=1;i<=m;++i){
        int u,v; u=read(); v=read();
        ban[++ecnt]=u; ban[++ecnt]=v;
        edg[u].push_back(v);
        edg[v].push_back(u);
    }
    
    for(register int i=1;i<=n;++i) sort(edg[i].begin(),edg[i].end());
    
    if(n==m+1){
        dfs1(1,1);
        return 0;	
    }
    else{
        // for(register int i=1;i<=n;++i){
        //     ++cur;
        //     for(register int j=1;j<=n;++j) vis[j]=false;
        //     dfs2(1,1);
            
       	// 	bool flag=false;
        //     if(ans[i][0]!=n){
        //         continue;	
        //     }
        //     if(ans[0][0]!=n){
        //         for(register int j=0;j<=ans[cur][0];++j) ans[0][j]=ans[i][j];
        //         continue;	
        //     }
        //     for(register int j=1;j<=n;++j){
        //         if(ans[0][j]<ans[i][j]){
        //         	flag=true;
        //         	break;
        //         }
        //         if(ans[0][j]>ans[i][j]) break;
        //     }
            
        //     if(flag) continue;
        //     for(register int j=0;j<=ans[cur][0];++j) ans[0][j]=ans[cur][j];
        //     }
        	
        // 	for(register int i=1;i<=n;++i) printf("%d ",ans[0][i]);
    	
    	dfs3(1,0); dfs4(1,0,0);
    	for(int i=1;i<=n;++i) printf("%d ",ans[i]);

    }
    
    return 0;	
}

int read(){
    char tmp=getchar(); int sum=0; bool flag=false;
    while(tmp<'0'||tmp>'9'){
        if(tmp=='-') flag=true;
        tmp=getchar();		
    }	
    while(tmp>='0'&&tmp<='9'){
        sum=sum*10+tmp-'0';
        tmp=getchar();		
    }
    return flag?-sum:sum;
}

void insert(int from,int to,int id){
    edge[id]=to; nxt[id]=head[from]; head[from]=id;	
}

void dfs1(int u,int f){
    printf("%d ",u);
    vis[u]=true;
    for(int i=0;i<edg[u].size();++i){
        int v=edg[u][i];
        if(!vis[v]){
            dfs1(v,u);	
        }	
    }
}

void dfs2(int u,int f){
    // ans[cur][++ans[cur][0]]=u;
    vis[u]=true;
    int ban1=ban[(cur<<1)-1],ban2=ban[(cur<<1)];
    // for(int i=0;i<edg[u].size();++i){
    //     int v=edg[u][i];
    //     if(u==ban1&&v==ban2||u==ban2&&v==ban1) continue;
    //     if(!vis[v]){
    //         dfs2(v,u);	
    //     }	
    // }
    if(u==ban1){
    	for(int i=0;i<edg[u].size();++i){
        	int v=edg[u][i];
        	if(v==ban2) continue;
        	if(!vis[v]){
            	dfs2(v,u);	
        	}	
   		}
    }
    if(u==ban2){
    	for(int i=0;i<edg[u].size();++i){
        	int v=edg[u][i];
        	if(v==ban1) continue;
        	if(!vis[v]){
            	dfs2(v,u);	
        	}	
   		}
    }
    else{
    	for(int i=0;i<edg[u].size();++i){
        	int v=edg[u][i];
        	if(!vis[v]){
            	dfs2(v,u);	
        	}	
   		}
    }
}

int getnxt(int i,int j){
    while(++j,j<edg[i].size()&&vis2[edg[i][j]]);
    if(j<edg[i].size()) return j;
    else return 0;
}

void dfs3(int u,int f){
    vis1[u]=1;
    for(int i=0;i<edg[u].size();++i){
        int v=edg[u][i]; if(v==f) continue;

        if(!rt&&vis1[v]){
            vis1[u]=2; rt=v; u2=u;
        }
        else if(!vis1[v]) dfs3(v,u);

        if(vis1[v]==2&&v!=rt){
            vis1[u]=2;
            if(!u1&&u==rt) u1=v;
        }
    }
}

void dfs4(int u,int f,int mx){
    ans[++ans[0]]=u;
    vis2[u]=1;

    for(int i=0;i<edg[u].size();++i){
        int v=edg[u][i]; if(v==f||vis2[v]) continue;

        int nx=getnxt(u,i);

        if(mx&&!nx&&vis1[u]==2&&vis1[v]==2&&!vis2[u2]&&v>mx) return;
        if(u==rt&&v==u1){
            dfs4(v,u,edg[u][nx]);
        }
        else{
            dfs4(v,u,mx?(nx?edg[u][nx]:mx):0);
        }
    }
}

D2T1 赛道修建

题目:

C 城将要举办一系列的赛车比赛。在比赛前,需要在城内修建 mm 条赛道。

C 城一共有 n 个路口,这些路口编号为 1,2,…,n,有 n−1 条适合于修建赛道的双向通行的道路,每条道路连接着两个路口。其中,第 i 条道路连接的两个路口编号为 \(a_i\)\(b_i\),该道路的长度为 \(l_i\)。借助这 n-1n−1 条道路,从任何一个路口出发都能到达其他所有的路口。

一条赛道是一组互不相同的道路 \(e_1,e_2,...,e_k\),满足可以从某个路口出发,依次经过 道路 \(e_1,e_2,...,e_k\)(每条道路经过一次,不允许调头)到达另一个路口。一条赛道的长度等于经过的各道路的长度之和。为保证安全,要求每条道路至多被一条赛道经过。

目前赛道修建的方案尚未确定。你的任务是设计一种赛道修建的方案,使得修建的 m 条赛道中长度最小的赛道长度最大(即 m 条赛道中最短赛道的长度尽可能大)

好好说话:

说实话,感觉题目挺明了的,就不解释了

题解:

一看,"求最小值的最大值",十有八九是二分答案

二分答案很重要的部分就是判断当前答案是否可性。由于已经二分\(\log n\)了,所以可以完整的跑一遍dp,不用担心超时。

那么dp怎么处理?设\(f_k\)表示以k节点为根的子树可以为k的父节点提供的最大长度。因为到点i,能组成经过点i的赛道必定不会超过两条。(赛道不可以掉头,一条道只能被一条赛道使用),所以可以考虑将\(f_k|k\in son of k\)两两配对,要求最小的要配对和可以大于等于二分值且最小的那一个边,原理也是不言而喻的。

那有没有可能其实当前的链不应该配对,而是应该留着给上面使用的?

答案是不会的,当前的链不论如何,只会为答案贡献1,不论所处何处。

确定思路之后,引入了multiset这个完美适用于这个情况的STL(虽然说我本人比较抵制使用STL,但是考试时间就是生命,真像)

multiset是set的一种“类型”,即支持重复元素的set,总结一下其用法

.insert(),即简单的插入一个数字

.size(),即当前multiset中剩余的元素个数

.empty(),即当前multiset是否为空

.erase(i),去掉所有大小为i的元素

.find(i),返回一个大小为i的位置指针

.lower_bound(i),返回一个大于等于i的元素位置指针

multiset<int>::iterator 即multiset位置指针的命名方式

代码如下:

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
#include <set>
using namespace std;

const int MAX=5e4+5;
int n,m;
int edge[MAX<<1],head[MAX],nxt[MAX<<1],w[MAX<<1],upper_dis;
int dis[MAX];
int limit,cnt,f[MAX];

int read();
void insert(int,int,int,int);
void getdis();
void maxlength(int,int,int);
bool check();
int dfs(int,int);
int binary(int,int,int,int);

multiset <int> son[MAX];

int main(){
    //freopen("test.in","r",stdin);

    n=read(); m=read();

    for(int i=1;i<n;++i){
        int u,v,w; u=read(); v=read(); w=read();
        insert(u,v,w,(i<<1)-1); insert(v,u,w,(i<<1));
    }

    getdis();
    // cout<<upper_dis<<endl;

    int l=0,r=upper_dis;
    while(l<=r){
        int mid=(l+r)>>1;
        limit=mid;
        if(check()) l=mid+1;
        else r=mid-1;
    }
    printf("%d",r);
    
    return 0;
}

int read(){
    char tmp=getchar(); int sum=0; bool flag=false;
    while(tmp<'0'||tmp>'9'){
        if(tmp=='-') flag=true;
        tmp=getchar();
    }
    while(tmp>='0'&&tmp<='9'){
        sum=(sum<<3)+(sum<<1)+tmp-'0';
        tmp=getchar();
    }
    return flag?-sum:sum;
}

void insert(int from,int to,int wei,int id){
    edge[id]=to; nxt[id]=head[from]; head[from]=id; w[id]=wei;
}

void getdis(){
    maxlength(1,1,0);
    int top=0,cur;
    for(int i=1;i<=n;++i){
        if(top<dis[i]){
            top=dis[i]; cur=i;
        }
    }
    dis[cur]=0;
    maxlength(cur,cur,0);
    int tar; top=0;
    for(int i=1;i<=n;++i){
        if(top<dis[i]){
            top=dis[i]; tar=i;
        }
    }
    upper_dis=dis[tar];
}

void maxlength(int u,int f,int d){
    dis[u]=d;

    int n,top=0;
    for(int i=head[u];i;i=nxt[i]){
        int v=edge[i]; if(v==f) continue;
        maxlength(v,u,d+w[i]);
    }
}

bool check(){
    cnt=0;
    dfs(1,1);
    if(cnt>=m) return true;
    else return false;
}

int dfs(int u,int fa){
    son[u].clear();
    for(int i=head[u];i;i=nxt[i]){
        int v=edge[i]; if(v==fa) continue;
        int tmp=dfs(v,u)+w[i];
        if(tmp>=limit) cnt++;
        else son[u].insert(tmp);
    }
    int l,r; l=0; r=son[u].size()-1;
    int mx=0;
    while(!son[u].empty()){
        if(son[u].size()==1){
            return max(mx,*son[u].begin());
        }
        multiset<int>::iterator it=son[u].lower_bound(limit-*son[u].begin());
        if(it==son[u].begin()&&son[u].count(*it)==1) it++;
        if(it==son[u].end()){
            mx=max(mx,*son[u].begin());
            son[u].erase(son[u].find(*son[u].begin()));
        }
        else{
            cnt++;
            son[u].erase(son[u].find(*it));
            son[u].erase(son[u].find(*son[u].begin()));
        }
    }
    return mx;
}

int binary(int u,int l,int r,int tar){
    // while(l<=r){
    // 	int mid=(l+r)<<1;
    // 	if(son[u][mid]>=tar) r=mid-1;
    // 	else l=mid+1;
    // }
    // return l;
}
~~

## 未完待续

可能永远也不会完结了`(*>﹏<*)′

太难了~~ 学了这么多,当年的noip还是题解最香
posted @ 2020-06-29 22:50  ticmis  阅读(158)  评论(0编辑  收藏  举报