NOIP2018(普及组 ) 赛后感想 & 题解

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。


文章目录

  • NOIP2018(普及组 ) 赛后感想 & 题解
    • #1. 标题统计
    • #2. 龙虎斗
    • #3. 摆渡车
        • Lemma
    • #4. 对称二叉树
    • #5. 总结

NOIP2018(普及组 ) 赛后感想 & 题解

\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad by frankchenfu 2018/11/24

#1. 标题统计

近年来普及组第一题的难度的确在逐年下降。这道题主要考察基础的字符操作(或者说是循环语句的使用)。

我们可以考虑每次读入一个字符,如果这个字符是数字或是字母,我们把答案 + 1 +1 +1,直到读完(读入EOF)为止。

因此我们可以写出以下代码(title.cpp)。时间复杂度 O ( n ) \text{O}(n) O(n),其中 n n n表示输入文件的大小。

#include
#include
#include
const int MAXN=10;

int main(){
	freopen("title.in","r",stdin);
	freopen("title.out","w",stdout);
	int ans=0;    
	char ch=getchar();
	while(ch!=EOF){
		ans+=(isdigit(ch)||isalpha(ch));
		ch=getchar();
	}
	printf("%d\n",ans);
	return 0;
}

#2. 龙虎斗

我们首先可以把出现在 p 1 p_1 p1兵营的“天降神兵”看作是 p 1 p_1 p1里原有的,这并不影响我们的答案。

接着,我们把轩轩的气势值记为正数,凯凯的记为负数,那么就是求对于某一个点使得加入 s 2 s_2 s2位工兵之后使得双方的气势值之和的绝对值最小。

因此我们可以写出以下代码(fight.cpp)。时间复杂度 O ( n ) \text{O}(n) O(n)

#include
#include
const int MAXN=100010;

int n,m,p1;
long long s1,s2;
long long c[MAXN];

inline long long my_abs(long long x){
	return x>0?x:-x;
}
int main(){
	freopen("fight.in","r",stdin);
	freopen("fight.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&c[i]);
	scanf("%d%d%lld%lld",&m,&p1,&s1,&s2);
	long long sum=0; 
	for(int i=1;i<=n;i++)
		sum+=c[i]*(long long)(m-i);
	sum+=s1*(m-p1); 
	long long ans=(1ll<<60),ans_pos=0;
	for(int i=1;i<=n;i++){ 
		if(ans>my_abs(sum+s2*(long long)(m-i))){
			ans=my_abs(sum+s2*(long long)(m-i));
			ans_pos=i;
		}
	}
	printf("%lld\n",ans_pos);
	return 0;
}

#3. 摆渡车

传说中让人抓狂的普及组第三题来了,就是这道题让我没有时间去做第 4 4 4题,导致我惨淡收场。

好吧开始讲题。我们首先考虑如何设计dp的状态。

  • 有一维表示到第 i i i个人了,这一定是要枚举的。接下来呢?

  • 我们还需要知道每个人在哪个时间出发,于是我们又多了一维。

于是我们设计了一个 f ( i , j ) f(i,j) f(i,j)的状态,表示 i i i个人在等待了 j j j分钟后出发。

至于为什么是等待的时间呢?因为实际上题目要求的是等待的时间,并非出发的时间,因此只与每个人等待的时间有关;另一方面,由于时间 t t t过大,空间无法承受,因此只能够将等待时间记入状态——当然这不影响答案。

所以等待的时间需要记多大的范围呢?

Lemma

对于每一个人,等待的时间一定在 [ 0 , m ) [0,m) [0,m)之间。

Proof. 对于一个人如果等待了 k m + t ( k ∈ N* ) km+t(k\in \text{N*}) km+t(kN*)的时间,则一定是等待别人一起拼车(否则他就自己走了)。那么如果他选择在 ( k − 1 ) m + t (k-1)m+t (k1)m+t的时间就出发了,那么他所等待的朋友一定可以在 k m + t km+t km+t的时间上车,而他自己却节省了 m m m的等待时间,因此对于一个人,他只可能等待 0 0 0 m − 1 m-1 m1分钟。

所以我们第二维状态只需要记 O ( m ) O(m) O(m)的大小。总的空间复杂度 O ( n m ) O(nm) O(nm)

接下来考虑转移。

我们枚举 i i i,设从第 j + 1 j+1 j+1个人开始都和 i i i拼车,并且 j j j推迟了 g g g分钟出发,那么 i i i出发的时间 t = min ⁡ ( a i , ( a j + g ) + m ) t=\min(a_i,(a_j+g)+m) t=min(ai,(aj+g)+m)。由此可以推出转移方程:

(注:其中 t t t就是算出来的出发时间,推出当第 j + 1 j+1 j+1个人开始都与 i i i拼车,第 j j j个人推迟 g g g分钟出发的最优答案)
f ( i , t − a i ) = ∑ k = j + 1 i t − a k = ( i − j ) ⋅ t − ∑ k = j + 1 i a k \begin{matrix} f(i,t-a_i)&=&\displaystyle\sum_{k=j+1}^{i}t-a_k\\&=&(i-j)\cdot t-\displaystyle\sum_{k=j+1}^{i}a_k \end{matrix} f(i,tai)==k=j+1itak(ij)tk=j+1iak

其中 ∑ a k \sum a_k ak是可以前缀和预处理的,因此我们只需要枚举 i , j , g i,j,g i,j,g即可。答案是 min ⁡ j = 0 m − 1 f ( n , j ) \min_{j=0}^{m-1}f(n,j) minj=0m1f(n,j)

时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)。然后这道题就结束了。

(偷偷告诉你,实际上如果你忘了前缀和,写出了 O ( n 3 m ) O(n^3m) O(n3m)的做法也是可以过的哦。NOIP实测可过。)

那我就把我考场上写的复杂度不太对但是满分的代码贴上来咯还是贴复杂度正确的吧(bus.cpp):

#include
#include
#include
const int MAXN=510;

int n,m,a[MAXN];
long long f[MAXN][110];
long long sum[MAXN];

inline int max(int x,int y){
	return x>y?x:y;
}
inline long long min(long long x,long long y){
	return x<y?x:y;
}

int main(){
	scanf("%d%d",&n,&m); 
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	std::sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i];
		for(int g=0;g<m;g++){
			long long wt=0;
			for(int k=1;k<=i;k++)
				wt+=a[i]+g-a[k];
			f[i][g]=wt;
		}
		for(int j=1;j<i;j++)
			for(int g=0;g<m;g++){
				int t=max(a[j]+m+g,a[i]);
				int lst=t-a[i];
				long long wt=(i-j)*(long long)t-(sum[i]-sum[j]);
				f[i][lst]=min(f[i][lst],f[j][g]+wt);
			}
	}
	long long _ans=(1ll<<60);
	for(int i=0;i<m;i++)
		_ans=min(_ans,f[n][i]);
	printf("%lld\n",_ans);
	return 0;
}

#4. 对称二叉树

由于第三题的缘故我没想到这道题怎么做。其实挺简单的。就是先算出每棵子树的大小,然后挨个判断。

判断方法:每次比较两颗子树,设根节点分别为 l , r l,r l,r

  • 要判断两个大小是不是相等。
  • 要判断权值是不是相等。
  • 然后判断直接连接的结构是不是对称——即比较 l . l s o n l.lson l.lson r . r s o n r.rson r.rson是同时存在; l . r s o n l.rson l.rson r . l s o n r.lson r.lson是同时存在。
  • 如果都满足,那么表示他们在目前这个深度上对称,递归比较 l . l s o n l.lson l.lson r . r s o n r.rson r.rson l . r s o n l.rson l.rson r . l s o n r.lson r.lson即可。
  • 如果全部满足,则就是对称二叉树。

因此,我们可以枚举每个点,然后递归判断以它为根的子树,初始的时候比较 i . l s o n i.lson i.lson i . r s o n i.rson i.rson

容易看出,由于有结构比较上的剪枝,因此最坏情况就是满二叉树和完全二叉树。这个时候判断的次数是 O ( log ⁡ 2 n ) O(\log_2 n) O(log2n) 层的。因此总的复杂度就是 O ( n log ⁡ 2 n ) O(n \log_2 n) O(nlog2n)。实际上几乎跑不满,因此 1 0 6 10^6 106数据还是能过的。

代码如下(tree.cpp):

#include
#include
const int MAXN=1000010;
int n,lson[MAXN],rson[MAXN];
int w[MAXN],sz[MAXN];

void dfs(int u){
    sz[u]=1;
    if(~u[lson]){
        dfs(u[lson]);
        sz[u]+=sz[u[lson]];
    }
    if(~u[rson]){
        dfs(u[rson]);
        sz[u]+=sz[u[rson]];
    }
}

bool check(int l,int r){
    if((~l)&&(~r)){
        if(w[l]!=w[r]||sz[l]!=sz[r])
            return 0;
        return check(l[lson],r[rson])&&check(l[rson],r[lson]);
    }
    else if((~l)|(~r))
        return 0;
    else
        return 1;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&i[lson],&i[rson]);
    dfs(1);
    int ans=0;
    for(int i=1;i<=n;i++)
        if(sz[i]>ans&&check(i[lson],i[rson]))
            ans=sz[i];
    printf("%d\n",ans);
    return 0;
} 

#5. 总结

​ 这一年感觉对于dp与搜索方面的训练是有效果的,尤其是考场上推出了第三题,感觉还是蛮有成就感的。但是代码熟练度还是不够,经常会犯一些小的错误,导致写不顺,最后没时间打第4题,还是能力有欠缺吧。希望以后思维上能有提高,不要让这种题目一题卡上2个小时。最后一次普及组之旅,还算是满意吧。

你可能感兴趣的:(题解)