1. 程式人生 > >NOIP 2018 普及組 解題報告

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 對稱二叉樹

完結撒花!!!