NOIP2018 复盘

NOIP2018 复盘

前言

在这里立一个可能无法实现的flag:

把NOIP从古至今(luogu上有)的每一年都写一篇复盘!!!

伏拉格综合征开始了

在复盘就不讲那些伤心的话了。

D1T1 铺设道路

考试时居然不知道这道题是原题。。。

一共有两种做法:

  1. 递推/贪心。

    设一个数组\(f\),顺序遍历。是这么更新的:

    \[f[i]=f[i-1]+max(0,a[i]-a[i-1])\]

    反正我没做过原题想不出来

  2. 分治。

    弄一个递归的函数,暴力统计区间最小值,暴力区间减,再来一个遍历找出断点,把所有的答案加起来就完事了。

    但是据说这种做法是会被卡成\(O(n^2)\)的。但是幸好NOIP的数据没卡。

    不然我就省四退役了

代码:(会被卡的做法2)

#include
#include
#define ll long long
const ll maxn = 100005;
ll a[maxn], n;

ll solve(ll l, ll r)
{
    if(l > r) return 0;
    ll ans = 0, minv = 0x3f3f3f3f;
    for(ll i = l; i <= r; i++) minv = std::min(minv, a[i]);
    for(ll i = l; i <= r; i++) a[i] -= minv;
    ans += minv;
    ll pos = l;
    while(a[pos] != 0 && pos <= r) pos++;
    if(pos == r + 1) return ans;
    ans += solve(l, pos - 1);
    ans += solve(pos + 1, r);
    return ans;
}
int main()
{
    //freopen("test.in", "r", stdin);
    scanf("%lld", &n);
    for(ll i = 1; i <= n; i++) scanf("%lld", &a[i]);
    printf("%lld\n", solve(1, n));
    return 0;
}

D1T2 货币系统

最初的想法是在一个大一点的范围内看看表示的会不会一样多。

不知道为什么就发现:只要看看能否表示出给你的所有货币。

从小到大排序,选到能表达出所有货币为止。

表示方法有两种

  1. dfs暴力搞。

    暴力枚举出每一个数前面乘的数,看看能否表达就是了。

    但是这种做法因为效率不高而只能搞80pts。

  2. dp背包方案。

    因为任何一种货币都能选到够,所以这不就是完全背包吗?

    所以使用完全背包,从能够表达的状态转移到另一个状态即可。

    同时,这个dp数组是可以循环利用的。如果每次枚举选几种货币的话会T掉。

    这个就是正解了。

代码:

#include
#include
#include

const int maxa = 25005, maxn = 105;
bool dp[maxa];
int a[maxn];
int n, ans;

bool check()
{
    for(int i = 1; i <= n; i++)
    {
        if(!dp[a[i]]) return false;
    }
    return true;
}
int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        //clearlove();
        scanf("%d", &n);
        for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
        std::sort(a + 1, a + n + 1);
        memset(dp, 0, sizeof dp);
        dp[0] = 1;
        ans = 0;
        for(int i = 1; i <= n; i++)
        {
            if(dp[a[i]]) continue;
            for(int j = a[i]; j <= 25000; j++)
            {
                dp[j] |= dp[j - a[i]];
            }
            ans++;
            if(check())
            {
                printf("%d\n", ans);
                break;
            }
        }
    }
    return 0;
}

D1T3 赛道修建

updated in Feb. 6th 2019.

这道题大致思路十分清晰:二分答案!二分枚举那个最小长度。

先看所有的部分分:

  • \(m=1\):直接求树的直径即可。20pts到手

  • \(b_i=a_i+1\):一条链。同P1182:二分答案之后扫一边数组来check。

  • \(a_i=1\):菊花图。这是一个比较重要的思路,会这个思路就能做题了:

    如果图是菊花图,那至少取一条边,顶多取两条边。

    大思路依旧是二分答案。设当前需要判定答案为\(mid\)时满不满足。

    把所有边的权值都挑出来排个序。大于等于\(mid\)的边单独成赛道,小于\(mid\)在里面找。用贪心的思想,这个被配对的权值越小越好。

    代码实现等会再告诉你。。。

之后就是正解了:

二分最小赛道长度,设要判定答案为\(mid\)

随便弄个点当根,设\(f(i)\)为以\(i\)为根的子树中未匹配的经过\(i\)的最长路径。

给每个点都建立一个multiset,用来最大匹配所有的半赛道。

为什么要每个点建一个?两个经过同一个点的半赛道才能合成一个完整赛道!

在dfs时,设遍历到\(v\)点,\(f(v)+weight(u,v)\)\(mid\)分两类讨论:

  • \(f(v)+weight(u,v) \geq mid\)时,赛道数++。
  • 否则,放入\(u\)点的multiset里面维护。

dfs完之后遍历每个点的multiset,每一次在里面找出最小权值,lower_bound出对应的iterator,如果满足条件就把两个半赛道都删掉,然后赛道数++;否则把这条半赛道抛弃掉,因为这条赛道没办法与剩下的任何赛道匹配。

最大的赛道数判断是否能够大于等于\(m\)即可,这样就能check出答案来了。

代码:(里面关于multiset的运用有得学)

#include
#define ll long long
const int maxn = 50005;
std::vector > G[maxn];
std::multiset s[maxn];
int n, m;

// get the diameter of tree
int dep[maxn];
void bfs(int s, int &maxdep, int &idx) {
    std::queue q;
    memset(dep, -1, sizeof dep);
    dep[s] = 0; q.push(s);
    while(!q.empty()) {
        int u = q.front(); q.pop();
        if(dep[u] > maxdep) {
            maxdep = dep[u]; idx = u;
        }
        for(auto it : G[u]) {
            int v = it.first, w = it.second;
            if(dep[v] == -1) {
                dep[v] = dep[u] + w; q.push(v);
            }
        }
    }
}
int get_diameter() {
    int maxdep = -1, idx = -1;
    bfs(1, maxdep, idx);
    int temp = idx;
    maxdep = idx = -1;
    bfs(temp, maxdep, idx);
    return maxdep;
}
int res;
int dfs(int u, int f, int mid) {
    s[u].clear();
    for(auto it : G[u]) {
        int v = it.first, w = it.second;
        if(v == f) continue;
        int temp = dfs(v, u, mid) + w;
        if(temp >= mid) {
            res++;
        } else {
            s[u].insert(temp);
        }
    }
    int ret = 0;
    while(!s[u].empty()) {
        int temp = *s[u].begin();
        if(s[u].size() == 1) {
            ret = std::max(ret, temp);
            break;
        }
        auto it = s[u].lower_bound(mid - temp);
        if(it == s[u].begin() && s[u].count(*it) == 1) ++it;
        if(it == s[u].end()) {
            ret = std::max(ret, temp);
            s[u].erase(s[u].find(temp));
        } else {
            res++;
            s[u].erase(s[u].find(temp));
            s[u].erase(s[u].find(*it));
        }
    }
    return ret;
}
bool check(int mid) {
    res = 0;
    dfs(1, 0, mid);
    return res >= m;
}
int main() {
    scanf("%d %d", &n, &m);
    for(int i = 1; i < n; i++) {
        int u, v, w; scanf("%d %d %d", &u, &v, &w);
        G[u].push_back(std::make_pair(v, w));
        G[v].push_back(std::make_pair(u, w));
    }
    int left = 0, right = get_diameter(), ans = -1;
    while(left <= right) {
        int mid = (left + right) >> 1;
        if(check(mid)) ans = mid, left = mid + 1;
        else right = mid - 1;
    }
    printf("%d\n", ans);
    return 0;
}

D2T1 旅行

这道题让我认识了什么叫做基环树!

这道题就是两个部分分:\(m=n-1\)\(m=n\)

\(m=n-1\)部分明显就是一棵树,那需要看怎么遍历这棵树才能得到答案。

仔细观察可以发现:把所有的边从小到大排序,然后从1节点开始遍历即可。60pts到手!

剩下的\(m=n\)部分分就是重点了。

基环树有这么几个性质:

  1. 基环树断了一条边可能就是一棵树。
  2. 基环树有且只有一个环。

再看到数据范围:\(1 \leq n \leq 5000\)

结合去年的i7 8700k,不由得让你想到了\(n^2\)算法!

所以算法出来了:枚举所有的边,每次断掉其中的一条边,在新图上面dfs,求出最小字典序的答案。

还想优化?先跑一遍tarjan的点双,若一条边的两个端点属于同个点双即为环上的边,在上面断边,会去掉那些不是树的断法。

在luogu上面这两种做法开O2都是能过的。

加强版不会做

代码:

#include
using std::cin;
using std::cout;
using std::endl;
#define ll long long
#define pii pair
const int maxn = 5005;
std::vector G[maxn];
bool vis[maxn];
int n, m;
int dfn[maxn], low[maxn], dtot;
int col[maxn], ctot;
std::stack sta;
std::vector answers, results;
std::pii edges[maxn];
bool zidianxu() {
    if(answers.size() == 0) return true;
    for(int i = 0; i < n; i++) {
        if(results[i] < answers[i]) return true;
        if(results[i] > answers[i]) return false;
    }
    return false;
}
void dfs(int u, int f, int nou, int nov) {
    results.push_back(u);
    for(auto v : G[u]) if(v != f) {
        if(u == nou && v == nov) continue;
        if(v == nou && u == nov) continue;
        dfs(v, u, nou, nov);
    }
}
void tarjan(int u, int f) {
    dfn[u] = low[u] = ++dtot;
    sta.push(u); vis[u] = true;
    for(auto v : G[u]) {
        if(v == f) continue;
        if(!dfn[v]) {
            tarjan(v, u); low[u] = std::min(low[u], low[v]);
        } else if(vis[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if(dfn[u] == low[u]) {
        // print
        //cout << "circle is below:" << endl;
        ctot++;
        while(sta.top() != u) {// only enter here is circle
            int sb = sta.top(); sta.pop(); vis[sb] = false;
            col[sb] = ctot;
            //cout << sb.first << ' ' << sb.second << endl;
        }
        int sb = sta.top(); sta.pop(); vis[sb] = false;
        col[sb] = ctot;
        //cout << sb.first << ' ' << sb.second << endl;
    }
}
int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= m; i++) {
        int u, v; scanf("%d%d", &u, &v);
        G[u].push_back(v); G[v].push_back(u);
        edges[i] = std::make_pair(u, v);
    }
    for(int i = 1; i <= n; i++) std::sort(G[i].begin(), G[i].end());
    if(m == n - 1) {
        dfs(1, 0, 0, 0);// tree don't need vis
        if(zidianxu()) answers = results;
    } else if(m == n) {
        for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, 0);
        //for(auto it : circle) cout << it.first << ' ' << it.second << endl;
        // change one edge
        for(int i = 1; i <= m; i++) {
            if(col[edges[i].first] == col[edges[i].second]) {
                results.clear();
                dfs(1, 0, edges[i].first, edges[i].second);
                // print
                //for(auto it : results) cout << it << ' ';
                //cout << endl;
                if(zidianxu()) answers = results;
            }
        }
    } else {
        assert(true);
    }
    bool first = true;
    for(auto it : answers) {
        if(first) first = false;
        else printf(" ");
        printf("%d", it);
    }
    printf("\n");
    return 0;
}

D2T2 填数游戏

待填。。。

D2T3 保卫王国

动态dp。这辈子都不可能达到这种高度了。

哎。

最后

谨以此纪念爆炸的NOIP2018

55555555

转载于:https://www.cnblogs.com/Garen-Wang/p/10352389.html

你可能感兴趣的:(NOIP2018 复盘)