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. OpenAPI 接口幂等实现

    OpenAPI 接口幂等实现 1.幂等性是啥? 进行一次接口调用与进行多次相同的接口调用都能得到与预期相符的结果. 通俗的讲,创建资源或更新资源的操作在多次调用后只生效一次. 2.什么情况会需要保证幂 ...

  2. 亚马逊云 RDB数据故障转移(多可用区)

    RDB关系数据库(Relational Database,RDB) 创建名为VPC for RDS的vpc 两个可用区,两组公内网 创建安全组 创建RDS数据库实例用的数据库子网组 创建RDS数据库实 ...

  3. wpf 手指触摸图片放大缩小 设置放大缩小值

    xaml代码: <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/w ...

  4. 你给文字描述,AI艺术作画,精美无比!附源码,快来试试!

    作者:韩信子@ShowMeAI 深度学习实战系列:https://www.showmeai.tech/tutorials/42 TensorFlow 实战系列:https://www.showmeai ...

  5. iOS- 最全的真机测试教程

      想要上架的同学请看:<iOS-最全的App上架教程> 因为最近更新了Xcode 8 ,证书的创建都大同小异,只是在Xcode 8中的设置有一些变化,我就在下面补充,如有什么疑问,请联系 ...

  6. 基于PCIe DMA的8通道视频采集&显示IP,兼容V4L2

    基于PCIe DMA的8通道视频采集&显示IP,兼容V4L2 Video Capture&Display IP for V4L2 在主机端视频设备内核驱动V4L2 的控制和调度下,Vi ...

  7. 词向量word2vec(图学习参考资料)

    介绍词向量word2evc概念,及CBOW和Skip-gram的算法实现. 项目链接: https://aistudio.baidu.com/aistudio/projectdetail/500940 ...

  8. Seata Server 1.5.2 源码学习

    Seata 包括 Server端和Client端.Seata中有三种角色:TC.TM.RM,其中,Server端就是TC,TM和RM属Client端.Client端的源码学习上一篇已讲过,详见 < ...

  9. 嵌入式-C语言基础:指针是存放变量的地址,那为什么要区分类型?

    指针是存放变量的地址,那为什么要区分类型?不能所有类型的变量都用一个类型吗?下面用一个例子来说明这个问题. #include<stdio.h> int main() { int a=0x1 ...

  10. 2022春每日一题:Day 40

    题目:[NOI2010] 超级钢琴 前求出美妙值的前缀和,然后倍增处理一下前缀和的最大值,然后对于一个左端点s,他能取到右端点的只有s+l到s+r,而他的最大贡献就是s+l 到s+r的最大子段和,因此 ...