NOIP2017普及组解题报告

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NOIP2017普及组解题报告相关的知识,希望对你有一定的参考价值。

NOIP2017普及组解题报告

T1:成绩(score)

这题作为普及组的第一题,没有任何难度,以至于我的同学们都开玩笑说,这题考的就是freopen的用法。但是成绩公布了之后,很多人都是30分、60分,原因是Linux下的浮点误差。虽然CCF后面说会浮点误差不在考察范围内,会重新评测,但是这也是一个教训,以后我对浮点数会更加小心。

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4  
 5 int main() {
 6     freopen("score.in","r",stdin);
 7     freopen("score.out","w",stdout);
 8     int a,b,c;
 9     cin>>a>>b>>c;
10     int s;
11     s=a/10*2+b/10*3+c/10*5;
12     cout<<s<<endl;
13     fclose (stdin);
14     fclose (stdout);
15     return 0;
16 }

T2:图书管理员(librarian)

在T1浮点误差的陷阱下,这题莫名成了这次普及组最水的一题。唯一需要注意的地方就是这比其他三个题目都长,而且没见过的文件名。我直接用数字来处理。题目里给了需求码的长度,所以简单很多,不然还要自己写个函数判断位数。

另外,有个小技巧:求一个正整数的最后x位,只要把这个数对10x取模即可。

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <algorithm>
 4 using namespace std;
 5  
 6 const int maxn=1000;
 7 int n,m;
 8 int a[maxn+1];
 9 int cifang[9];
10  
11 void init() {
12     cin>>n>>m;
13     for (int i=1; i<=n; i++)
14         scanf("%d",&a[i]);
15     sort(a+1,a+1+n);
16     cifang[0]=1;
17     for (int i=1; i<=8; i++)
18         cifang[i]=cifang[i-1]*10;
19 }
20  
21 int main() {
22     freopen("librarian.in","r",stdin);
23     freopen("librarian.out","w",stdout);
24     init();
25     for (int i=1; i<=m; i++) {
26         int changdu,x;
27         bool o=false;
28         scanf("%d %d",&changdu,&x);
29         for (int j=1; j<=n; j++)
30             if (a[j]%cifang[changdu]==x) {
31                 cout<<a[j]<<endl;
32                 o=true;
33                 break;
34             }
35         if (o==false)
36             cout<<"-1"<<endl;
37     }
38     fclose (stdin);
39     fclose (stdout);
40     return 0;
41 }

T3:棋盘(chess)

本题我用的是暴力dfs。对于每一个点,枚举向上、下、左、右四个方向移动的每一种情况,一直搜索下去,直到无路可走或者到达终点。那应该怎么决定应该把没有颜色的格子变为什么颜色呢?让我们列举每一种情况:① 当连接的两个格子颜色相同时,最好的方法是把中间的格子变为与这两个格子相同的颜色; ② 当连接两个格子颜色不同时,那随意变颜色都可以。因为我们不知道将要搜索到的格子的下一个颜色是什么,所以我们需要找出一个共同具有的特点。稍加思考,便可得出,只要把这个格子变为前一个格子的颜色,就符合最优条件了。

在考场上,我稍作判断,深搜的时间复杂度为O(节点数×转移的数量)。本题中节点数为m2,每一次dfs往四个方向走,所以转移数是4。对于最大的m=100,时间显然还绰绰有余。

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5  
 6 const int maxm=100;
 7 int m,n;
 8 int a[maxm+1][maxm+1];
 9 int ans[maxm+1][maxm+1];
10  
11 void kuaidu(int &p) {
12     char c;
13     p=0;
14     do c=getchar();
15     while (c<0||c>9);
16     do p=p*10+c-0, c=getchar();
17     while (c>=0&&c<=9);
18 }
19  
20 void init() {
21     cin>>m>>n;
22     memset(a,-1,sizeof(a));
23     memset(ans,0x7F,sizeof(ans));
24     for (int i=1; i<=n; i++) {
25         int x,y,z;
26         kuaidu(x);
27         kuaidu(y);
28         kuaidu(z);
29         a[x][y]=z;
30     }
31     ans[1][1]=0;
32 }
33  
34 void dfs(int x, int y, int col) {
35     if (x==m&&y==m)
36         return;
37     //其实还有一种向量数组的写法,但考场上为了保险,宁愿写长一点
38     if (x<m) {
39         int s=0;
40         if (a[x+1][y]!=col)
41             s++;
42         if (a[x+1][y]==-1)
43             s++;
44         if ((a[x+1][y]!=-1||a[x][y]!=-1)&&ans[x+1][y]>ans[x][y]+s) {
45             ans[x+1][y]=ans[x][y]+s;
46             dfs(x+1,y,a[x+1][y]==-1?a[x][y]:a[x+1][y]);
47         }
48     }
49     if (y<m) {
50         int s=0;
51         if (a[x][y+1]!=col)
52             s++;
53         if (a[x][y+1]==-1)
54             s++;
55         if ((a[x][y+1]!=-1||a[x][y]!=-1)&&ans[x][y+1]>ans[x][y]+s) {
56             ans[x][y+1]=ans[x][y]+s;
57             dfs(x,y+1,a[x][y+1]==-1?a[x][y]:a[x][y+1]);
58         }
59     }
60     if (x>1) {
61         int s=0;
62         if (a[x-1][y]!=col)
63             s++;
64         if (a[x-1][y]==-1)
65             s++;
66         if ((a[x-1][y]!=-1||a[x][y]!=-1)&&ans[x-1][y]>ans[x][y]+s) {
67             ans[x-1][y]=ans[x][y]+s;
68             dfs(x-1,y,a[x-1][y]==-1?a[x][y]:a[x-1][y]);
69         }
70     }
71     if (y>1) {
72         int s=0;
73         if (a[x][y-1]!=col)
74             s++;
75         if (a[x][y-1]==-1)
76             s++;
77         if ((a[x][y-1]!=-1||a[x][y]!=-1)&&ans[x][y-1]>ans[x][y]+s) {
78             ans[x][y-1]=ans[x][y]+s;
79             dfs(x,y-1,a[x][y-1]==-1?a[x][y]:a[x][y-1]);
80         }
81     }
82 }
83  
84 int main() {
85     freopen("chess.in","r",stdin);
86     freopen("chess.out","w",stdout);
87     init();
88     dfs(1,1,a[1][1]);
89     if (ans[m][m]==0x7F7F7F7F)
90         cout<<"-1"<<endl;
91     else
92         cout<<ans[m][m]<<endl;
93     fclose (stdin);
94     fclose (stdout);
95     return 0;
96 }

在10000个点中,最多只有1000个点有颜色,比较稀疏。所以可以使用另一种做法——最短路求解。

首先要知道一个概念,叫“曼哈顿距离”:

出租车几何或曼哈顿距离(Manhattan Distance)是由十九世纪的赫尔曼·闵可夫斯基所创词汇 ,是种使用在几何度量空间的几何学用语,用以标明两个点在标准坐标系上的绝对轴距总和。

在平面上,坐标(x1, y1)的i点与坐标(x2, y2)的j点的曼哈顿距离为:
d(i,j)=|x1-x2|+|y1-y2|.

那么,怎么建图呢?依照题意,对于曼哈顿距离为1的两个格子,如果颜色相同,则连一条权值为0的无向边;若颜色不同,则连一条权值为1的无向边。同理,对于曼哈顿距离为2的两个格子,如果颜色相同,则连一条权值为2的无向边;若颜色不同,则连一条权值为3的无向边。然后就可以跑最短路了……吗?

题目当中仅保证起点有颜色,可没保证终点有颜色啊。如果终点没有颜色,那么怎么走最短路也走不了。所以对终点要有特判:如果终点没有颜色,则要把与终点的曼哈顿距离为1的点向终点连一条权值为2的有向边。如果没做这一步,会一直输出“-1”,这就叫“细节决定成败”。

T4:跳房子(jump)

这题是第四题,对于我这种蒟蒻来讲是不可能AC的,所以先想着骗分。什么时候不能得到目标分数呢?如果所有正数分数都加起来还小于目标分数,那就得不到目标分数。所以先特判“-1”。

再看看这一题,我想起了NOIP2015提高组的“跳石头”,那题不是二分吗?这题也好像啊……于是果断使用二分求解。确定下来二分了,那么就来考虑,怎样判断二分的这个答案可不可以,显然使用动态规划。

dp[i]表示跳到第i个格子时所能得到的分数最大值,若跳不到该格,则dp[i]=-1。为了动规方便,再加入起点的格子,显然,起点格子离原点的距离为0,格子上的值也为0。状态转移方程:max(l,r)表示[l,r]区间内能跳到的格子中的最大值,则dp[i]=max(点i到原点的距离-mid,点i到原点的距离+mid)。用没有优化的DP,我交上了我的考场代码。

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5 
 6 const int maxn=500000;
 7 struct gezi {
 8     int juli;
 9     int zhi;
10 } a[maxn+1];
11 long long dp[maxn+1];
12 int n,d,k,lbound=1,rbound,ans; //考场上写的代码太粗心,此处有误,二分的左边界应该为0,幸好官方数据没有答案为0的数据
13 long long sum;
14 
15 void kuaidu(int &p) {
16     char c;
17     int f=1;
18     p=0;
19     do {
20         c=getchar();
21         if (c==-)
22             f=-1;
23     } while (c<0||c>9);
24     do p=p*10+c-0, c=getchar();
25     while (c>=0&&c<=9);
26     p=p*f;
27 }
28 
29 void init() {
30     cin>>n>>d>>k;
31     for (int i=1; i<=n; i++) {
32         kuaidu(a[i].juli);
33         kuaidu(a[i].zhi);
34         if (a[i].zhi>0)
35             sum+=a[i].zhi;
36     }
37     rbound=a[n].juli;
38 }
39 
40 long long dynamic_programming(int zuo, int you) {
41     memset(dp,-1,sizeof(dp));
42     dp[0]=0;
43     for (int i=1; i<=n; i++)
44         for (int j=0; j<i; j++)
45             if (a[i].juli-a[j].juli>=zuo&&a[i].juli-a[j].juli<=you&&dp[j]!=-1)
46                 dp[i]=max(dp[i],dp[j]+a[i].zhi);
47     long long num=0;
48     for (int i=1; i<=n; i++)
49         if (dp[i]>num)
50             num=dp[i];
51     return num;
52 }
53 
54 int main() {
55     freopen("jump.in","r",stdin);
56     freopen("jump.out","w",stdout);
57     init();
58     if (sum<k) {
59         cout<<"-1"<<endl;
60         return 0;
61     }
62     while (lbound<=rbound) {
63         int mid=(lbound+rbound)/2;
64         int zuobianjie=max(1,d-mid);
65         int youbianjie=d+mid;
66         long long num=dynamic_programming(zuobianjie,youbianjie);
67         if (num<k)
68             lbound=mid+1;
69         else {
70             ans=mid;
71             rbound=mid-1;
72         }
73     }
74     cout<<ans<<endl;
75     fclose (stdin);
76     fclose (stdout);
77     return 0;
78 }

这样DP的时间复杂度是二维的,对于50%的数据可以过,但是对于另外50%的数据n=500000,即使两秒的时限也是超得不爱超了。考后,我在想怎么优化动态规划。

我听到一个叫单调队列的东西,专门取区间内的最大最小值。在POJ上,有一题叫做“Sliding Window”,就是单调队列的入门题。单调队列要想优化DP,必须得保证lr是单调递增或递减的。而在本题中,在向右DP时,上述的状态转移方程在dp[i]=max(点i到原点的距离-mid,点i到原点的距离+mid)的mid不变的情况下,随着i离原点越来越远,lr越来越大,所以也是单调递增的。这样一优化,DP的时间复杂度就降至一维,对于最大的n=500000,就能在2秒的时限内轻松通过了。

改进后的DP代码:

 1 long long dynamic_programming(int zuo, int you) {
 2     memset(dp,-1,sizeof(dp));
 3     dp[0]=0;
 4     memset(q,0,sizeof(q));
 5     int tou=1, wei=0, j=0;
 6     for (int i=1; i<=n; i++) {
 7         while (a[i].juli-a[j].juli>=zuo&&j<i) {
 8             if (dp[j]!=-1) {
 9                 while (tou<=wei&&dp[q[wei]]<=dp[j])
10                     wei--;
11                 q[++wei]=j;
12             }
13             j++;
14         }
15         while (tou<=wei&&a[i].juli-a[q[tou]].juli>you)
16             tou++;
17         if (tou<=wei)
18             dp[i]=dp[q[tou]]+a[i].zhi;
19     }
20     long long num=0;
21     for (int i=1; i<=n; i++)
22         if (dp[i]>num)
23             num=dp[i];
24     return num;
25 }

 

总体而言,这次NOIP普及组没有什么算法,考的都是细节与技巧,全比赛难度最大的只是单调队列,而且单调队列在今年的夏令营里也讲过了,应当会灵活运用。只要在考场上够专注、认真,有充足的自信与扎实的编程基础,350分绝对不是问题。

以上是关于NOIP2017普及组解题报告的主要内容,如果未能解决你的问题,请参考以下文章

NOIP1999普及组解题报告

NOIP2002普及组解题报告

NOIP2012普及组 (四年后的)解题报告 -SilverN

NOIP1998普及组解题报告

NOIP2016普及组复赛解题报告

NOIP2001普及组解题报告