NOIP 2018 普及组 解题报告

今年的题目出的有点诡异,难度跨越有点大

入门  to 普及- to(注意:前方东非大裂谷,请小心慢行) 提高+/省选- to 提高+/省选-

不过实际上没有这么难

T3、T4 一个DP 一个暴力(虽然不是正解) 也就可以过了

扯入正题

T1 标题统计

这道题十分的水,没有什么技术含量,随便怎么搞都可以过。

下面是我直接放代码了。。。

#include<bits/stdc++.h>
using namespace std;

char t;
int ans(0);

int main(){
    freopen( "title.in", "r", stdin );
    freopen( "title.out", "w", stdout );
    while( ( t = getchar() ) != EOF )
        if ( t != ' ' && t != '\n' && t != '\r' ) ans++;
    printf( "%d", ans ); 
    return 0;
}
T1 标题统计

T2 龙虎斗

这道题没话说,只是题目长了点,好好理解一下也是不难的。

我们可以预处理出两边阵营的气势和(别忘了加上“某一刻天降神兵”)然后枚举每个兵营,把你的兵加进去,算出之后两个阵营最终的气势,然后选出气势之差绝对值最小的哪个阵营就可以了。

话不多说,直接上代码(普及- 及以下难度的不用具体讲吧?)。还有注意要开 long long(死了也别忘记)据说没开long long只能得70左右。

#include<bits/stdc++.h>
using namespace std;

typedef long long LL;

LL n, m, p1, s1, s2;
LL s[166666];
LL L, R;
LL ans(-1), q(0x7f7f7f7f7f7f7f7f);

LL Abs( LL x ){
    return x >= 0 ? x : -x;
}

int main(){
    freopen( "fight.in", "r", stdin );
    freopen( "fight.out", "w", stdout );
    scanf( "%lld", &n );
    for ( int i = 1; i <= n; ++i ) scanf( "%lld", &s[i] );
    scanf( "%lld%lld%lld%lld", &m, &p1, &s1, &s2 );
    s[p1] += s1;
    for ( int i = 1; i <= n; ++i ){
        if ( i < m ) L += ( m - i ) * s[i];
        if ( i > m ) R += ( i - m ) * s[i];
    }
    for ( int i = 1; i <= n; ++i ){
        LL tL(L), tR(R), c;
        if ( i < m ) tL += ( m - i ) * s2;
        if ( i > m ) tR += ( i - m ) * s2;
        c = Abs( tL - tR );
        if ( c < q ) ans = i, q = c;
    }
    printf( "%lld\n", ans );
    return 0;
}
T2 龙虎斗

T3 摆渡车

看这道题的时候,我(相信大家也是这样)最先想到的是贪心,但是从数据范围可以看出,如果是贪心题,数据范围不会那么小(相信NOIP不会和Luogu月赛一样,2018 11月月赛 搞个几百大小数据骗我们用DP,结果是贪心)。有些人会想(including me),是不是在有人到达时才能发车呢???没想清楚就下手的话,就会浪费好多时间。仔细想想,很容易发现不一定要有人到达时发车,比如有时候,bus一回来,有个人等了2分钟,后面那个人还有INF(hh) min 才会来,如果有人到达时才能发车,那么bus将在INF min后才等到一个人,原来等了一分钟的那个人与司机等得花都谢了,所以这时候肯定是一回来就发车,虽然没有人刚好到达。

我们可以考虑,如果两个人之间的时间差距 >= m 时 可以把差距直接变成 2m,因为bus最多等 m min(因为如果等的时间大于m,足够让bus往返一次,要更优),若前一个人的前1 min发车,那么bus回来开始等是(m - 1) min之后(以前一个人为基准),再等m min,也就是说最优方案肯定是在每个人来到时间的2m-1 min之内发过车,方便一点直接看成2m。

然后,DP。f[i]代表i时发过车的最优方案每个人等的时间和。

f[i] = min{ f[j] + (j + 1 到 i 之间到达的所有人等的时间之和) }(j <= i - m)

对于(j + 1 到 i 之间到达的所有人等的时间之和)的值,我们可以预处理出来,即将到达时间求前缀和,人数求前缀和,可以化为 (j + 1 到 i 之间到达的所有人人数和 * i) - (j + 1 到 i 之间到达的所有人来到的时间之和) 。

根据以上结论(bus最多等m min),我们可以进一步缩小j的范围,上次发车肯定在i - m之后。

这样的复杂度已经降到了O(m^2 *n)本来这样已经可以了,但是还有更优的做法,可以把复杂度进一步降到O(mn)级别。

比赛时我每想到缩小j的范围,我顺着贪心的思路:能不能用贪心来优化DP呢???于是就乱搞了一个"GDP"(greedy DP)(国民生产总值?hh)然后侥幸过了所有测试点。

我们可以考虑,当bus发车时(设为i)没有人到达,bus肯定刚刚回来,i - m肯定发过车。因为如果i之前就回来了,为什么不早点发车呢?如果早1 min,不但会影响上车人数,而且每人少等1 min何乐而不为?

所以i没有人刚到,i 发过车,但i - m每发过车绝对不是最优的,不管怎样,直接当做j只有一个取值——i - m。

这样做,虽然不能保证中间过程达到最优,但因为遵循最优的方案,结果不会出现错误。

然后上代码!(虽然不是最好的解,但89ms也凑合吧。 至少比原来几百ms好多了)

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 500005;

int n, m, tot;
int a[666], c[666], f[MAXN], p[MAXN];
int dp[MAXN], ans;
bool v[MAXN];
int M;

int main(){
    freopen( "bus.in", "r", stdin );
    freopen( "bus.out", "w", stdout );
    scanf( "%d%d", &n, &m );
    for ( int i = 1; i <= n; ++i ) scanf( "%d", &a[i] );
    sort( a + 1, a + n + 1 );
    for ( int i = 1; i <= n; ++i ) c[i] = a[i] - a[i - 1];
    for ( int i = 1; i <= n; ++i ){
        if ( c[i] >= 2 * m ) c[i] = 2 * m;
        a[i] = a[i - 1] + c[i];
    }
    for ( int i = 1; i <= n; ++i ){
        M = max( M, a[i] );
        v[a[i]] = 1, f[a[i]] += a[i], p[a[i]]++;
    }
    for ( int i = 1; i <= M + m; ++i ) f[i] += f[i - 1], p[i] += p[i - 1];
    memset( dp, 0x7f, sizeof dp );
    dp[0] = 0;
    for ( int i = 1; i <= M + m; ++i ){
        if ( i <= m ){
            dp[i] = p[i] * i - f[i]; continue;
        }
        if ( !v[i] ){
            dp[i] = dp[i - m] + ( p[i] - p[i - m] ) * i - ( f[i] - f[i - m] ); continue;
        }
        for ( int j = max( 0, i - 2 * m ); j <= i - m; ++j ){
            dp[i] = min( dp[i], dp[j] + i * ( p[i] - p[j] ) - ( f[i] - f[j] ) );
        }
    }
    int ans(0x7f7f7f7f);
    for ( int i = M; i <= M + m; ++i ) ans = min( ans, dp[i] );
    printf( "%d\n", ans );
    return 0;
}
T3 摆渡车

T4 对称二叉树

这道题我也不知道正解是什么。我直接暴力+剪枝也跑过了所有测试点(数据太水?)

我们可以考虑当将一棵树所有节点的左右子树交换,那么搜索的顺序左变右,右变左。

即原来对于节点P,从根节点到P的路径是左右左左右,那么反转后根节点到P'的位置的路径就是右左右右左。

我们直接枚举每个子树的根节点,把原来的点、翻转后的点一一对应就可以了。

实现起来也不难。注意加点剪枝(不解释剪枝原理了)。

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1000000 + 0xac;//AC万岁!!!

int n, v[MAXN], d[MAXN], s[MAXN];
unsigned long long M1[MAXN], M2[MAXN], M3[MAXN];
int L[MAXN], R[MAXN];

void DFS( int x, int dep ){
    s[x] = 1; M1[x] = v[x]; M2[x] = v[x]; M3[x] = dep * v[x];
    if ( L[x] != -1 ) DFS( L[x], dep + 1 ), s[x] += s[L[x]], M1[x] *= M1[L[x]], M2[x] += M2[L[x]], M3[x] += M3[L[x]];
    d[x] = dep;
    if ( R[x] != -1 ) DFS( R[x], dep + 1 ), s[x] += s[R[x]], M1[x] *= M1[R[x]], M2[x] += M2[R[x]], M3[x] += M3[R[x]];
}

bool check( int x, int y ){
    if ( v[x] != v[y] || s[x] != s[y] || M1[x] != M1[y] || M2[x] != M2[y] || M3[x] != M3[y] ) return 0;
    if ( L[x] > 0 || R[y] > 0 ){
        if ( L[x] < 0 || R[y] < 0 ) return 0;
        if ( !check( L[x], R[y] ) ) return 0;
    }
    if ( R[x] > 0 || L[y] > 0 ){
        if ( R[x] < 0 || L[y] < 0 ) return 0;
        if ( !check( R[x], L[y] ) ) return 0;
    }
    return 1;
}

bool cmp( int x, int y ){
    return s[x] > s[y];
}

int main(){
    freopen( "tree.in", "r", stdin );
    freopen( "tree.out", "w", stdout );
    scanf( "%d", &n );
    for ( int i = 1; i <= n; ++i ) scanf( "%d", &v[i] );
    for ( int i = 1; i <= n; ++i ) scanf( "%d%d", &L[i], &R[i] );
    DFS( 1, 1 );
    int ans(1);
    for ( int i = 1; i <= n; ++i )
        if ( s[i] > ans && check( i, i ) ) ans = max( ans, s[i] );
    printf( "%d\n", ans );
    return 0;
}
T4 对称二叉树

完结撒花!!!

猜你喜欢

转载自www.cnblogs.com/louhancheng/p/9973934.html