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