NOIP 2017 宝藏

题目链接

https://www.luogu.org/problemnew/solution/P3959

这道题学到很多很多东西

还是很多收获的,虽然花了非常长的时间

首先直接写prim可以得45分,还是很多的

貌似不需要long long

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int MAXN = 20;
int g[MAXN][MAXN]; 
int vis[MAXN], n, m;
ll dis[MAXN], d[MAXN], ans;

ll prim(int u)
{
    memset(vis, 0, sizeof(vis));
    vis[u] = 1;
    _for(i, 1, n) d[i] = i == u ? 0 : 1e9;
    _for(i, 1, n) dis[i] = i == u ? 0 : 1e9;
    
    _for(v, 1, n)
        if(g[u][v] != -1)
        {
            d[v] = min(d[v], (ll)g[u][v]);
            dis[v] = 1;
        }
    
    ll res = 0;
    REP(k, 1, n)
    {
        ll mint = 1e18;
        int v;
        _for(i, 1, n)
            if(!vis[i] && d[i] != 1e9 && mint > d[i])
                mint = d[i], v = i;

        vis[v] = 1;
        res += mint;
        
        _for(i, 1, n)
        {
            if(vis[i] || g[v][i] == -1) continue;
            dis[i] = min(dis[i], dis[v] + 1);
            if(d[i] > g[v][i] * (dis[v] + 1))
            {
                d[i] = g[v][i] * (dis[v] + 1);
                dis[i] = dis[v] + 1;
            }
        }
    }
    
    return res;
}

int main()
{
    memset(g, -1, sizeof(g));
    scanf("%d%d", &n, &m);
    
    _for(i, 1, m)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        if(g[u][v] == -1) g[u][v] = g[v][u] = w;
        else g[u][v] = g[v][u] = min(g[u][v], w);
    }
    
    ans = 1e18;
    _for(i, 1, n) ans = min(ans, prim(i));
    printf("%lld
", ans);
    
    return 0;
}

然后直接打搜索竟然可以拿到70分!!

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

const int MAXN = 20;
int g[MAXN][MAXN]; 
int vis[MAXN], n, m;
int d[MAXN], ans;

void dfs(int cnt, int now)
{
    if(now >= ans) return;
    if(cnt == n)
    {
        ans = min(ans, now);
        return;
    }
    
    _for(i, 1, n)
    {
        if(!vis[i]) continue;
        _for(j, 1, n)
            if(!vis[j] && g[i][j] != -1)
            {
                d[j] = d[i] + 1;
                vis[j] = 1;
                dfs(cnt + 1, now + g[i][j] * d[j]);
                d[j] = vis[j] = 0; 
            }
    }
}

int main()
{
    memset(g, -1, sizeof(g));
    scanf("%d%d", &n, &m);
    
    _for(i, 1, m)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        if(g[u][v] == -1) g[u][v] = g[v][u] = w;
        else g[u][v] = g[v][u] = min(g[u][v], w);
    }
    
    ans = 1e9;
    _for(i, 1, n)
    {
        vis[i] = 1;
        dfs(1, 0);
        vis[i] = 0;
    }
    printf("%d
", ans);
   
    return 0;
}

然后接下来要AC这道题就有很多方法了

有一个错误的状压可以AC,比搜索快在记忆化上

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

const int MAXN = 5000;
int g[MAXN][MAXN]; 
int dp[MAXN], n, m;
int d[MAXN];

void dfs(int S)
{
    REP(i, 0, n)
    {
        if(!(S & (1 << i))) continue;
        REP(j, 0, n)
        {
            if(S & (1 << j) || g[i][j] == -1) continue;
            if(dp[S] + g[i][j] * d[i] < dp[S | (1 << j)])
            {
                dp[S | (1 << j)] = dp[S] + g[i][j] * d[i];
                d[j] = d[i] + 1;
                dfs(S | (1 << j));
            }
        }
    }
}

int main()
{
    memset(g, -1, sizeof(g));
    scanf("%d%d", &n, &m);
    
    _for(i, 1, m)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w); u--; v--;
        if(g[u][v] == -1) g[u][v] = g[v][u] = w;
        else g[u][v] = g[v][u] = min(g[u][v], w);
    }

    int ans = 1e9;
    REP(i, 0, n)
    {
        memset(dp, 0x3f, sizeof(dp));
        memset(d, 0, sizeof(d));
        d[i] = 1;
        dp[1 << i] = 0;
        dfs(1 << i);
        ans = min(ans, dp[(1 << n) - 1]);
    }
    printf("%d
", ans);
   
    return 0;
}

还有一个骗分的random_shuffle可以AC掉

因为这道题做一次贪心的时间非常非常小,所以可以随机化做非常非常多次的贪心,取最优

随机化的一个方法就是random_shuffle,重置排列

对于这道题可以拿这个排列作为开拓的顺序,然后贪心就好了。

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

const int MAXN = 20;
int g[MAXN][MAXN]; 
int vis[MAXN], n, m;
int d[MAXN], ans;

void dfs(int cnt, int now)
{
    if(now >= ans) return;
    if(cnt == n)
    {
        ans = min(ans, now);
        return;
    }
    
    _for(i, 1, n)
    {
        if(!vis[i]) continue;
        _for(j, 1, n)
            if(!vis[j] && g[i][j] != -1)
            {
                d[j] = d[i] + 1;
                vis[j] = 1;
                dfs(cnt + 1, now + g[i][j] * d[j]);
                d[j] = vis[j] = 0; 
            }
    }
}

int main()
{
    memset(g, -1, sizeof(g));
    scanf("%d%d", &n, &m);
    
    _for(i, 1, m)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        if(g[u][v] == -1) g[u][v] = g[v][u] = w;
        else g[u][v] = g[v][u] = min(g[u][v], w);
    }
    
    ans = 1e9;
    _for(i, 1, n)
    {
        vis[i] = 1;
        dfs(1, 0);
        vis[i] = 0;
    }
    printf("%d
", ans);
   
    return 0;
}

然后是正解,比较难理解

 据说这道题本来是T3??

思路网上都有,我讲几个我一直卡住的点

正解是按照深度一层一层来dp的

这个dp是不关注根在哪里的,只关注点的集合和深度

这点非常重要

首先不一定深度浅的优,因为边权不一样

然后这个深度只可能算大,不可能算小,因为转移的时候要用到一个cost数组(具体见程序)

如果算小的话说明这个集合不可能是这个深度,那么这个cost数组的值也就会非常大,表示不存在

所以顶多就是没有这么深但是你算得时候算这么深

不过这样不会漏掉最优解,因为你把所有的情况都枚举了一遍

还学到了许多技巧,如3^n枚举子集,用从小到大用lowbit作dp

虽然写了一天,但还是收获很大的。不求写题的数量,但求学到知识,有进步

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

const int MAXN = 13;
const int MAXM = (1 << 12) + 10;
int lg[MAXN], cost[MAXM][MAXM];
int g[MAXN][MAXN], dp[MAXN][MAXM];
int n, m;

inline int lowbit(int x) { return x & (-x); }

inline void down(int& a, int b) { a = min(a, b); }

int main()
{
    memset(g, -1, sizeof(g));
    scanf("%d%d", &n, &m);
    
    _for(i, 1, m)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w); u--; v--;
        if(g[u][v] == -1) g[u][v] = g[v][u] = w;
        else g[u][v] = g[v][u] = min(g[u][v], w);
    }
    
    int Smax = (1 << n) - 1;
    REP(i, 0, n) lg[1 << i] = i;
    
    REP(S, 1, 1 << n)
    {
        int T = Smax ^ S; //表示补集 
        stack<int> st;
        for(int S0 = T; S0; S0 = (S0-1) & T) st.push(S0); //S0从小到大枚举,这样dp顺序才对 
        
        while(!st.empty())
        {
            int S0 = st.top(); st.pop();  
            int u = lg[lowbit(S0)], mint = 5e5 + 10; //如果这个mint要加很多次的话,就按题目的最大来 
            REP(v, 0, n)                              //不要省事写1e9,否则40分没了!! 
                if(S & (1 << v) && g[u][v] != -1) // 这里只转移lowbit这个点就好了,之前的最小值已经转移过了 
                    down(mint, g[u][v]);
            cost[S][S0] = cost[S][S0 ^ (1 << u)] + mint;  //注意这里是S转移到S0,通过S0中的点向S连边 
        }
    }
    
    memset(dp, 0x3f, sizeof(dp));
    REP(i, 0, n) dp[0][1 << i] = 0;
    _for(h, 1, n)
        REP(S, 1, 1 << n)
            for(int S0 = S; S0; S0 = (S0 - 1) & S)
                down(dp[h][S], dp[h-1][S ^ S0] + cost[S ^ S0][S0] * h); 
    
    int ans = 1e9;
    _for(h, 0, n) //注意这里深度可能为0,即一个点没有边,很坑 
        down(ans, dp[h][Smax]);
    printf("%d
", ans);
   
    return 0;
}
原文地址:https://www.cnblogs.com/sugewud/p/9888228.html