NOIP2018提高组题解

D1T1:铺设道路

回忆NOIP2013D2T1 积木大赛,发现这两题唯一的区别就是一个是造山一个是填坑,而把填坑的操作反序就是造山,所以可以直接使用那道题的方法。

具体方法是,从左到右每次考虑新的一列,若这一列的坑比左边一列浅,那么可以在填左边一列的时候顺便填好这个坑(只要把所有右端点为i-1的操作右端点全部改为i即可),不需要任何操作。若这一列的坑比左边深,那么就必须先将这一列的坑填到与左边平齐,再让左边的操作顺带把这个坑填平。

于是有:若a[i]<=a[i-1],ans不变,否则ans+=a[i]-a[i-1]。

期望得分:100

复杂度:$O(n)$

 1 #include<cstdio>
 2 #include<algorithm>
 3 #include<cstring>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 using namespace std;
 6 
 7 const int N=100010;
 8 int n,ans,a[N];
 9 
10 int main(){
11     freopen("road.in","r",stdin);
12     freopen("road.out","w",stdout);
13     scanf("%d",&n);
14     rep(i,1,n){
15         scanf("%d",&a[i]);
16         if (a[i]>a[i-1]) ans+=a[i]-a[i-1];
17     }
18     printf("%d\n",ans);
19     return 0;
20 }
road

D1T2:货币系统

这题性质十分类似线性基的构造,首先证明两个结论:

1.最简系统一定是原系统的子集。

2.最简系统中任何一个面额不能被其余面额表出。

结论二显然。

若最简系统出现了一个原系统没有的数x,那么若这个数能被原系统表出,那么它的加入没有任何意义,可以删去,矛盾。

若不能被表出,那么最简系统就比原系统多表出了一个数x,矛盾。

结论一得证。

于是就有了运行策略,先将原系统中所有数从小到大排序(因为能表出某数的面额一定都不比那个数大),逐一判断每个数能否被当前最简系统表出,若不能则加入。

那么如何判断能否被表出呢,注意到值域不大,那么这个题的原型实际上就是一个完全背包问题,于是每次加入数的时候做一次完全背包即可。

期望得分:100

复杂度:$O(n*a_i)$

 1 #include<cstdio>
 2 #include<algorithm>
 3 #include<cstring>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 using namespace std;
 6 
 7 const int N=2500010;
 8 int T,n,a[210];
 9 bool f[N];
10 
11 int main(){
12     freopen("money.in","r",stdin);
13     freopen("money.out","w",stdout);
14     for (scanf("%d",&T); T--; ){
15         scanf("%d",&n); int mx=0,ans=0;
16         rep(i,1,n) scanf("%d",&a[i]),mx=max(mx,a[i]);
17         rep(i,1,mx) f[i]=0; f[0]=1;
18         sort(a+1,a+n+1);
19         rep(i,1,n){
20             if (f[a[i]]) continue;
21             ans++;
22             rep(j,a[i],mx) f[j]|=f[j-a[i]];
23         }
24         printf("%d\n",ans);
25     }
26     return 0;
27 }
money

D1T3:赛道修建

十分套路的一道题。

算法一:m=1的情况就是要找一条直径。

期望得分:20

复杂度:$O(n)$

算法二:ai=1是一个菊花图,二分答案后尽量匹配出最多的长度>=mid的链。这个贪心或再套一层二分答案均可。

期望得分:20

复杂度:$O(n\log n)$

算法三:bi=ai+1是一条链,二分答案后贪心选取即可。若当前链长度已>=mid则断开,计数器+1。

期望得分:20

复杂度:$O(n\log n)$

以上部分通过数据分治可以得到55分。

算法四:分支不超过3,只要找到一个度为1的点作为根就是一棵二叉树。在树上做简单DP即可,具体可见满分做法。

期望得分:55

复杂度:$O(n\log n)$

以上部分分通过数据分治可以得到80分。

算法五:首先想到二分答案转化为可行性问题。问题转化为,能否找到至少m条长度不小于mid的不相交的道路。

考虑树形DP,这个问题要求最大化两个指标,一是道路条数最大化,二是道路条数最多的情况下每条道路长度最长。

于是设f[x]表示x的子树中最多能选出多少条长度不小于mid的互不相交的链,g[x]表示x的子树中,在已选出f[x]条合法链后,最多能从根往下延伸出多长的链(这条链不与已选出的f[x]条链相交,它将和x子树之外的点连接成为合法链)。

我们有结论:一个子树内,必定是先让合法链的个数最多(即最大化f),在此基础上再尽量让从根伸出的链最长(即最大化g)。

考虑转移,首先f[x]+=f[son],然后合并子树伸出的链。问题转化为找尽量多的不重复数对,是两两和不小于mid,且最后剩下来的最大的数最大。

先将所有数从小到大排序,然后从小到大考虑(若某个数已经配对则跳过),对于一个数x,在后面找到最小的数y使x+y>=mid,再找y以及y之后的第一个尚未配对的数x与之配对,f++,若不存在则x不与任何数配对,用x更新g。

可以证明,这个贪心策略一定是最优的,f和g在此策略下都能被最大化。

至于找y可以直接二分或单调指针扫描,找y后第一个可用的数可以用set或经典的并查集处理。

期望得分:100

复杂度:$O(n\log^2 n)$或$O(n\log n)$

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 #define For(i,x) for (int i=h[x],k; i; i=nxt[i])
 6 using namespace std;
 7 
 8 const int N=100010;
 9 int n,m,u,v,w,cnt,L,R,mid,ans,f[N],g[N],a[N],fa[N];
10 int h[N],to[N<<1],nxt[N<<1],val[N<<1];
11 void add(int u,int v,int w){ to[++cnt]=v; val[cnt]=w; nxt[cnt]=h[u]; h[u]=cnt; }
12 int get(int x){ return (fa[x]==x) ? x : fa[x]=get(fa[x]); }
13 
14 void DP(int x,int Fa){
15     f[x]=0; g[x]=0; int tot=0;
16     For(i,x) if ((k=to[i])!=Fa) DP(k,x),f[x]+=f[k];
17     For(i,x) if ((k=to[i])!=Fa) a[++tot]=g[k]+val[i];
18     if (!tot) return;
19     sort(a+1,a+tot+1);
20     while (tot && a[tot]>=mid) tot--,f[x]++;
21     if (!tot) return;
22     rep(i,1,tot+1) fa[i]=i;
23     rep(i,1,tot-1){
24         if (fa[i]!=i) continue;
25         int r=lower_bound(a+i+1,a+tot+1,mid-a[i])-a;
26         if (r>tot || a[i]+a[r]<mid) continue;
27         int t=get(r);
28         if (t>tot) continue;
29         f[x]++; fa[i]=get(i+1); fa[t]=get(t+1);
30     }
31     rep(i,1,tot) if (fa[i]==i) g[x]=a[i];
32 }
33 
34 int main(){
35     freopen("track.in","r",stdin);
36     freopen("track.out","w",stdout);
37     scanf("%d%d",&n,&m);
38     rep(i,2,n) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w),R+=w;
39     while (L<=R){
40         mid=(L+R)>>1; DP(1,0);
41         if (f[1]>=m) ans=mid,L=mid+1; else R=mid-1;
42     }
43     printf("%d\n",ans);
44     return 0;
45 }
track

D2T1:旅行

首先树的情况不难想到贪心策略,由于走的是一个dfs序,所以每次选最小的相邻的尚未走过的点走过去即可。

n=m的情况是一个环套树,由于最终的遍历过程中一定有一条边没有被遍历到,那么枚举这条边,删掉后再跑树的情况即可。

注意如果删的边不在环上会导致图不连通,此时显然得到的遍历序列长度小于n,判掉就好。

但这样n=m写的太丑的话可能会被卡常,所以最好在走跑dfs的时候实时判断是否比当前字典序是否比最优解大,若是则直接退出。

 1 #include<cstdio>
 2 #include<algorithm>
 3 #include<cstring>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 using namespace std;
 6 
 7 const int N=5010;
 8 int n,m,d,tot,cnt,res[N],tmp[N],ans[N],h[N],to[N<<1],nxt[N<<1],E[N][N];
 9 bool fl,fl1,vis[N];
10 struct edg{ int u,v; }e[N];
11 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; }
12 
13 void dfs(int x){
14     if (fl) return;
15     res[++tot]=x; vis[x]=1;
16     if (res[tot]<ans[tot]) fl1=1;
17     if (res[tot]>ans[tot] && !fl1) { fl=1; return; }
18     for (int i=h[x]; i; i=nxt[i]){
19         int k=to[i];
20         if (!((x==e[d].u && k==e[d].v) || (x==e[d].v && k==e[d].u)) && !vis[k]) dfs(k);
21     }
22 }
23 
24 int main(){
25     freopen("travel.in","r",stdin);
26     freopen("travel.out","w",stdout);
27     scanf("%d%d",&n,&m);
28     rep(i,1,n) ans[i]=n+1;
29     rep(i,1,m){
30         scanf("%d%d",&e[i].u,&e[i].v);
31         E[e[i].u][++tmp[e[i].u]]=e[i].v;
32         E[e[i].v][++tmp[e[i].v]]=e[i].u;
33     }
34     rep(i,1,n){
35         if (!tmp[i]) continue;
36         sort(E[i]+1,E[i]+tmp[i]+1);
37         for (int j=tmp[i]; j; j--) add(i,E[i][j]);
38     }
39     if (m==n-1){
40         dfs(1);
41         rep(i,1,n) printf("%d ",res[i]);
42         return 0;
43     }
44     for (d=1; d<=m; d++){
45         tot=0; rep(i,1,n) vis[i]=0;
46         fl=0; fl1=0; dfs(1);
47         if (tot<n || fl) continue;
48         rep(i,1,n) ans[i]=res[i];
49     }
50     rep(i,1,n) printf("%d ",ans[i]);
51     return 0;
52 }
travel_1

或者直接找到环,只删环上的边。这在环很小的时候效果极佳。

 1 #include<vector>
 2 #include<cstdio>
 3 #include<algorithm>
 4 #include<cstring>
 5 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 6 #define For(i,x) for (int i=h[x],k; i; i=nxt[i])
 7 using namespace std;
 8 
 9 const int N=5010;
10 int n,m,u,v,cnt,tim,tot,top,res[N],ans[N];
11 int cir[N],vis[N],pre[N],h[N],to[N<<1],nxt[N<<1];
12 vector<int>a[N];
13 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; }
14 
15 void dfs(int x){
16     vis[x]=1; res[++top]=x;
17     For(i,x) if (!vis[k=to[i]]) dfs(k);
18 }
19 
20 void find(int x){
21     vis[x]=++tim;
22     For(i,x) if (!vis[k=to[i]]) pre[k]=x,find(k);
23         else if (vis[k]>vis[x]){
24             for (int j=k; j!=x; j=pre[j]) cir[++tot]=j;
25             cir[++tot]=x;
26         }
27 }
28 
29 void dfs2(int x,int fa){
30     vis[x]=1; res[++top]=x;
31     For(i,x) if ((k=to[i])!=fa && !((x==u && k==v) || (x==v && k==u))) dfs2(k,x);
32 }
33 
34 int main(){
35     freopen("travel.in","r",stdin);
36     freopen("travel.out","w",stdout);
37     scanf("%d%d",&n,&m);
38     rep(i,1,m) scanf("%d%d",&u,&v),a[u].push_back(v),a[v].push_back(u);
39     rep(i,1,n){
40         sort(a[i].begin(),a[i].end());
41         for (int j=a[i].size()-1; j>=0; j--) add(i,a[i][j]);
42     }
43     if (m<n){ dfs(1); rep(i,1,n) printf("%d ",res[i]); return 0; }
44     find(1);
45     rep(i,1,n) ans[i]=n+1;
46     rep(i,1,tot){
47         top=0; rep(j,1,n) vis[j]=0;
48         u=cir[i]; v=cir[i%tot+1];
49         dfs2(1,0); bool flag=0;
50         rep(j,1,n) if (res[j]!=ans[j]){ flag=res[j]<ans[j]; break; }
51         if (flag) rep(j,1,n) ans[j]=res[j];
52     }
53     rep(i,1,n) printf("%d ",ans[i]);
54     return 0;
55 }
travel_2

D2T2:填数游戏

算法一:n,m<=3可以直接手算。

n=1时:$2^n$

m=1时:$2^m$

n=2,m=2:样例

n=2,m=3:手算得36

n=3,m=2:手算得36

n=3,m=3:样例

期望得分:20

复杂度:$O(1)$

开始理性分析下题目的性质:

题意是,对于任何一个点(x,y),从(x-1,y)出发的任意一条路径均不大于从(x,y-1)出发的任意一条路径。

那么有两个显然的结论:

1.不存在$w_{x-1,y}=1$,$w_{x,y-1}=0$的情况。

2.若存在x,y使得$w_{x-1,y}=w_{x,y-1}$,那么(x,y)右下方的所有对角线上每个数都相等,及所有离(x,y)的曼哈顿距离相等的数都相等。

结论一显然。

若存在某个点(x1,y1)和(x2,y2) (x1,x2>=x0,y1,y2>=y0)使得$w_{x1,y1}=1$,$w_{x2,y2}=0$且这两个点到(x0,y0)的曼哈顿距离相等,那么可以从(x-1,y)出发走到(x1,y1),从(x,y-1)出发走到(x2,y2),那么前者路径的字典序一定会大于后者。

结论二得证。

这样证明了两个结论的必要性,理性分析一下可以发现,两个结论合起来就是充要条件。证明大概是讨论多种情况,这里不再叙述。

于是我们有了:

算法二:n=2可以忽视结论2,其中(1,1)和(n,m)可以随意选择,其它每对数有(0,0),(1,0),(1,1)三种取法(限于结论一不能取(0,1))。

很容易得出答案为$4*3^(m-1)$。或者状压DP也行,这里只要压两位所以几乎相当于普通DP。

期望得分:30

复杂度:$O(\log m)$

以上部分分通过数据分治可以得到50分。

 1 #include<cstdio>
 2 #include<algorithm>
 3 #include<cstring>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 using namespace std;
 6 
 7 const int N=2000010,mod=1e9+7;
 8 int n,m,f[N][4];
 9 
10 int main(){
11     freopen("game.in","r",stdin);
12     freopen("game.out","w",stdout);
13     scanf("%d%d",&n,&m);
14     if (n==1 || m==1){ printf("%d\n",1<<(n*m)); return 0; }
15     if (n==3 && m==2){ puts("36"); return 0; }
16     if (n==3 && m==3){ puts("112"); return 0; }
17     if (n==5 && m==5){ puts("7136"); return 0; }
18     f[1][0]=f[1][1]=f[1][2]=f[1][3]=1;
19     rep(i,2,m){
20         f[i][0]=f[i][1]=((f[i-1][0]+f[i-1][1])%mod+(f[i-1][2]+f[i-1][3])%mod)%mod;
21         f[i][2]=f[i][3]=(f[i-1][1]+f[i-1][3])%mod;
22     }
23     printf("%d\n",((f[m][0]+f[m][1])%mod+(f[m][2]+f[m][3])%mod)%mod);
24     return 0;
25 }
game(50分)

算法三:n=3时,结论二只会限制2,3行的一些数对相等,考虑状压DP解决。

f[i][S][0/1]表示,考虑到第i列,第i列的状态为S (0<=S<8),是/否存在结论二限制 的方案数。转移考虑这一列和上一列是否会构成限制。

期望得分:15

复杂度:$O(64m)$

 1 #include<cstdio>
 2 #include<algorithm>
 3 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 4 using namespace std;
 5 
 6 const int N=1000010,mod=1e9+7;
 7 int n,m,ans,f[N][10][2];
 8 
 9 int main(){
10     freopen("game.in","r",stdin);
11     freopen("game.out","w",stdout);
12     scanf("%d%d",&n,&m); int ed=(1<<n)-1;
13     rep(S,0,ed) f[1][S][0]=1;
14     rep(i,1,m-1){
15         rep(S,0,ed) if (f[i][S][0] || f[i][S][1]){
16             rep(S1,0,ed){
17                 if ((S1&4 && !(S&2)) || ((S1&2) && !(S&1))) continue;
18                 if ((S1&4 && S&2) || (!(S1&4) && !(S&2))) f[i+1][S1][1]=(f[i+1][S1][1]+f[i][S][0])%mod;
19                     else f[i+1][S1][0]=(f[i+1][S1][0]+f[i][S][0])%mod;
20                 if (!(S1&2) && (S&1)) continue;
21                 f[i+1][S1][1]=(f[i+1][S1][1]+f[i][S][1])%mod;
22             }
23         }
24     }
25     rep(S,0,ed) ans=(ans+(f[m][S][0]+f[m][S][1])%mod)%mod;
26     printf("%d\n",ans);
27     return 0;
28 }
game(DP)

算法四:根据结论一和结论二给暴搜剪枝,可以在15s内轻松打完8*8的表。

期望得分:15

复杂度:打表后$O(1)$

 1 #include<cstdio>
 2 #include<algorithm>
 3 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 4 using namespace std;
 5 
 6 const int N=1010,mod=1e9+7;
 7 int n,m,ans,tot,a[N][N],d[N];
 8 struct P{ int x,y; }p[N];
 9 void dfs(int x,int y);
10 
11 void Do(int x,int y){
12     if (x>1 && y<m && a[x][y]==a[x-1][y+1]) p[++tot]=(P){x,y+1};
13     dfs(x,y+1);
14     if (x>1 && y<m && a[x][y]==a[x-1][y+1]) tot--;
15 }
16 
17 void dfs(int x,int y){
18     if (x>n){ ans++; return; }
19     if (y>m) { dfs(x+1,1); return; }
20     rep(i,1,tot) if (p[i].x<x && p[i].y<=y && y<m){
21         if (~a[x][y] && a[x][y]!=a[x-1][y+1]) { a[x][y]=-1; return; }
22         a[x][y]=a[x-1][y+1];
23     }
24     if (~a[x][y]){ Do(x,y); a[x][y]=-1; return; }
25     a[x][y]=1; Do(x,y); a[x][y]=0;
26     if (!(x>1 && y<m && a[x][y]<a[x-1][y+1])) Do(x,y);
27     a[x][y]=-1;
28 }
29 
30 int main(){
31     freopen("game.in","r",stdin);
32     freopen("game.out","w",stdout);
33     for (n=1; n<=8; n++){
34         for (m=1; m<=8; m++){
35             rep(i,1,n) rep(j,1,m) a[i][j]=-1;
36             ans=0; dfs(1,1); printf("%d ",ans);
37         }
38         puts("");
39     }
40     return 0;
41 }
game(打表)

以上部分分通过数据分治可以得到80分。

算法五:稍微把表打大一点,暴力找规律即可。不会证明,没什么好说的。

数据范围完全不知道出于什么意义。事实上出得多大都是没有问题的。

期望得分:100

复杂度:$O(\log n+\log m)$

 1 #include<cstdio>
 2 #include<algorithm>
 3 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 4 using namespace std;
 5 
 6 const int mod=1e9+7;
 7 const int d[10]={0,2,12,112,912,7136,56768,453504,3626752};
 8 int n,m;
 9 
10 int ksm(int a,int b){
11     int res=1;
12     for (; b; a=1ll*a*a%mod,b>>=1)
13         if (b & 1) res=1ll*res*a%mod;
14     return res;
15 }
16 
17 int calc(int n,int m){
18     if (n==m) return d[n];
19     if (n==1) return ksm(2,m);
20     if (n==2) return 4ll*ksm(3,m-1)%mod;
21     if (n==3) return 112ll*ksm(3,m-3)%mod;
22     return ((3ll*d[n]-3ll*ksm(2,n))*ksm(3,m-n-1)%mod+mod)%mod;
23 }
24 
25 int main(){
26     freopen("game.in","r",stdin);
27     freopen("game.out","w",stdout);
28     scanf("%d%d",&n,&m);
29     if (n>m) swap(n,m);
30     printf("%d\n",calc(n,m));
31     return 0;
32 }
game(100)

猜你喜欢

转载自www.cnblogs.com/HocRiser/p/9977020.html