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还是题解最香

NOIP2018 解题报告的更多相关文章

  1. NOIp2018解题报告

    D1: T1 \(Ans = \sum_{i=2}^{n} |a_{i}-a_{i-1}|\),正确性可由贪心证得 T2 考虑贪心,选出一个属于A的集合,容易证明其是最优的 然后考虑一个数如果不被选, ...

  2. NOIP2018初赛 解题报告

    前言 \(NOIP2018\)初赛已经结束了,接下来就要准备复赛了. 不过,在此之前,还是先为初赛写一篇解题报告吧. 单项选择题 送分题.(虽然我还是做错了)可以考虑将它们全部转化为\(10\)进制, ...

  3. NOIP 2018 普及组 解题报告

    目录 标题统计 题目链接 思路 代码 龙虎斗 题目链接: 思路 代码 摆渡车 题目链接: 思路 对称二叉树 题目链接 思路: 先来解释一下为毛现在才来发解题报告: 其实博主是参加过NOIP 2018普 ...

  4. CH Round #56 - 国庆节欢乐赛解题报告

    最近CH上的比赛很多,在此会全部写出解题报告,与大家交流一下解题方法与技巧. T1 魔幻森林 描述 Cortana来到了一片魔幻森林,这片森林可以被视作一个N*M的矩阵,矩阵中的每个位置上都长着一棵树 ...

  5. 二模13day1解题报告

    二模13day1解题报告 T1.发射站(station) N个发射站,每个发射站有高度hi,发射信号强度vi,每个发射站的信号只会被左和右第一个比他高的收到.现在求收到信号最强的发射站. 我用了时间复 ...

  6. BZOJ 1051 最受欢迎的牛 解题报告

    题目直接摆在这里! 1051: [HAOI2006]受欢迎的牛 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 4438  Solved: 2353[S ...

  7. 习题:codevs 2822 爱在心中 解题报告

    这次的解题报告是有关tarjan算法的一道思维量比较大的题目(真的是原创文章,希望管理员不要再把文章移出首页). 这道题蒟蒻以前做过,但是今天由于要复习tarjan算法,于是就看到codevs分类强联 ...

  8. 习题:codevs 1035 火车停留解题报告

    本蒟蒻又来写解题报告了.这次的题目是codevs 1035 火车停留. 题目大意就是给m个火车的到达时间.停留时间和车载货物的价值,车站有n个车道,而火车停留一次车站就会从车载货物价值中获得1%的利润 ...

  9. 习题: codevs 2492 上帝造题的七分钟2 解题报告

    这道题是受到大犇MagHSK的启发我才得以想出来的,蒟蒻觉得自己的代码跟MagHSK大犇的代码完全比不上,所以这里蒟蒻就套用了MagHSK大犇的代码(大家可以关注下我的博客,友情链接就是大犇MagHS ...

随机推荐

  1. springboot+redis+虚拟机 springboot连接linux虚拟机中的redis服务

    文章目录 1.前提条件:确保虚拟机开启.并且连接到redis 2.新建立一个springboot项目,创建项目时勾选web选项 3.在pom中引入redis依赖 4.在application.prop ...

  2. 齐博x1标签之异步加载标签数据

    为什么要异步加载标签?他有什么好处 如果一个页面的标签太多,又或者是页面中某一个标签调用数据太慢的话,就会拖慢整个页面的打开,非常影响用户体验.这个时候,用异步加载的话,就可以一块一块的显示,用户体验 ...

  3. ahk精简自用

    ;9:03 2022/8/20 自用 #NoEnv #Warn #SingleInstance Force ;设工作目录为桌面 SetWorkingDir %A_Desktop% ;托盘提示必须放在热 ...

  4. Python基础部分:9、数据的类型和内置方法

    目录 一.数据类型内置方法理论 1.什么是数据内置方法 2.如何调用数据内置方法 二.整型(int)内置方法与操作 1.类型转换 2.进制数转换 三.浮点型(float)内置方法与操作 1.类型转换 ...

  5. KMP算法,匹配字符串模板(返回下标)

    //KMP算法,匹配字符串模板 void getNext(int[] next, String t) { int n = next.length; for (int i = 1, j = 0; i & ...

  6. java判断手机号三大运营商归属的工具类

    package com.tymk.front.third; import java.util.regex.Pattern; public class OperatorsUtil { /** * 中国电 ...

  7. networkQuality

    基本使用 networkQuality 是一个命令行工具,需要使用「终端」App(或者你首选的其他终端模拟器)运行.方法是: 首先,点按「程序坞」(Dock)中的「启动台」(LaunchPad)图标, ...

  8. maple软件安装教程

    Maple2022适用于Win7/10/11(64位)系统,亲测可用! Maple2022 WIN10 64位安装步骤:1.先使用"百度网盘"下载MPE22_CN_x64安装包到电 ...

  9. 关于linux mint(nemo)添加右键菜单修改方法

    前言 其实在 linux mint 桌面上右键弹出的菜单,以及在资源管理器 nemo 中右键菜单,这些都是基于 nemo,进行的操作,所以更改右键菜单也就是更改nemo的配置文件 操作 在目录 /ho ...

  10. 整理 js 日期对象的详细功能,使用 js 日期对象获取具体日期、昨天、今天、明天、每月天数、时间戳等,以及常用的日期时间处理方法

    在 javascript 中内置了一个 Date 对象,可用于实现一些日期和时间的操作. 本文整理 js 日期对象的详细功能,使用 js 日期对象获取具体日期.昨天.今天.明天.每月天数.时间戳等,以 ...