洛谷P2387 [NOI2014] 魔法森林 题解

洛谷P2387 [NOI2014] 魔法森林 题解

题目链接:P2387 [NOI2014] 魔法森林

题意:每条边有边权 a , b a,b a,b 两个,求 1 1 1 n n n 的路径使得所经过的边中 max ⁡ { a } + max ⁡ { b } \max\{a\}+\max\{b\} max{a}+max{b} 尽可能小

由于下面写了很多解题思路上的东西,导致有些冗长,因此这里先放简化版题解

如果不理解可以跳过这一段简化版,直接看下面的完整版

简化版

因为不方便同时处理 a , b a,b a,b 两维,我们可以枚举 a a a 然后尽可能的选择较小的 b b b

这个 b b b 的一定在连接 1 1 1 n n n 的最小生成树上

因此这道题就是个动态加边的LCT裸题了

代码在后面,时间复杂度 O ( m log ⁡ m ) O(m\log m) O(mlogm)


完整版

这种“二维”的问题常见的解法就是枚举第一维,然后优化第二维

不知道这个套路也没关系

首先看到这个题意,最大值的和最小,显然我们无法确定这是个多大的值

似乎可以二分?但是再一想,二分 a + b a+b a+b 肯定是不对的

那先二分 a a a 再二分 b b b (二分套二分)呢?

看上去似乎也不行,这个 b b b 不是很好二分,而且复杂度似乎是 O ( n log ⁡ 3 n ) O(n\log^3n) O(nlog3n)

那二分 a a a 呢?好像要对 a a a 这一维从小到大排个序,然后在 a a a 相等时,第二维也从小到大排序

而每次我们检验 a a a 时,都要把在 a a a 所在位置(数组中)左边的边都去连连看有没有用什么的

那还不如直接从小到大枚举 a a a 呢!

于是我们可以枚举 a a a ,然后发现如果要保证 b b b 尽可能的小

我们需要在所有 a ’ < a a’<a a<a 的边中找出一棵生成树

也就是在用 a a a 所在位置(数组中)左边的边建出的图中建最小生成森林(因为这个子图不一定连通)

其实我们并不是在为了维护最小生成森林而维护它的

而是在维护包括结点 1 1 1 n n n 最小生成树的过程中所必需的 qwq

当这个包括结点 1 1 1 n n n 的最小生成树建出来的时候,我们就可以更新答案了

注意这里是更新答案,不是最终结果,因为最终结果它和 a a a 的大小没有直接的关系

举个例子就知道了,a=1,b=998244353a=2,b=1,显然是后者更小

那么怎么动态的维护一棵最小生成树呢?我们可以使用LCT

如果不会LCT动态维护最小生成树,可以看看这篇

那么,其实这道题就解决了

也就是枚举 a a a ,动态维护最小生成树

时间复杂度 O ( m log ⁡ m ) O(m\log m) O(mlogm)


代码如下

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define gc() getchar()
#define pc(a) putchar(a)
#define N (int)(3e5+5)
template<typename T>void read(T &k)
{
    char ch=gc();T x=0,f=1;
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=gc();}
    while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
    k=x*f;
}
template<typename T>void write(T k)
{
    if(k<0){k=-k;pc('-');}
    if(k>9)write(k/10);
    pc(k%10+'0');
}
int n,m,ans=INF;
namespace LCT
{
    struct Edge{int u,v,a,b;}e[N];
    int cmp(Edge a,Edge b){return a.a==b.a?a.b<b.b:a.a<b.a;}
    struct node
    {
        int ch[2],id,mx,w,fa,tag;
    }t[N];
    #define isroot(x) ((t[t[x].fa].ch[0]!=x)&&(t[t[x].fa].ch[1]!=x))
    void pushr(int x)
    {
        swap(t[x].ch[0],t[x].ch[1]);
        t[x].tag^=1;
    }
    void push_up(int x)
    {
        t[x].id=x;t[x].mx=t[x].w;
        if(t[x].ch[0]&&t[t[x].ch[0]].mx>t[x].mx)
            t[x].mx=t[t[x].ch[0]].mx,t[x].id=t[t[x].ch[0]].id;
        if(t[x].ch[1]&&t[t[x].ch[1]].mx>t[x].mx)
            t[x].mx=t[t[x].ch[1]].mx,t[x].id=t[t[x].ch[1]].id;
    }
    void push_down(int x)
    {
        if(t[x].tag)
        {
            if(t[x].ch[0])pushr(t[x].ch[0]);
            if(t[x].ch[1])pushr(t[x].ch[1]);
            t[x].tag^=1;
        }
    }
    void push_all(int x)
    {
        if(!isroot(x))push_all(t[x].fa);
        push_down(x);
    }
    void rotate(int x)
    {
        int y=t[x].fa;
        int z=t[y].fa;
        int k=t[y].ch[1]==x;
        if(!isroot(y))t[z].ch[t[z].ch[1]==y]=x;
        t[x].fa=z;
        t[y].ch[k]=t[x].ch[k^1];
        t[t[x].ch[k^1]].fa=y;
        t[x].ch[k^1]=y;
        t[y].fa=x;
        push_up(y);
        push_up(x);
    }
    void splay(int x)
    {
        push_all(x);
        while(!isroot(x))
        {
            int y=t[x].fa;
            int z=t[y].fa;
            if(!isroot(y))
            (t[z].ch[1]==y)^(t[y].ch[1]==x)?rotate(x):rotate(y);
            rotate(x);
        }
    }
    void access(int x)
    {
        for(int y=0;x;y=x,x=t[x].fa)
            splay(x),t[x].ch[1]=y,push_up(x);
    }
    void make_root(int x)
    {
        access(x);splay(x);
        pushr(x);
    }
    int find_root(int x)
    {
        access(x);splay(x);
        while(t[x].ch[0])push_down(x),x=t[x].ch[0];
        splay(x);
        return x;
    }
    void split(int x,int y)
    {
        make_root(x);
        access(y);splay(y);
    }
    void link(int x,int y)
    {
        make_root(x);
        if(find_root(y)!=x)t[x].fa=y;
    }
    void cut(int x,int y)
    {
        make_root(x);
        if(find_root(y)==x&&t[y].fa==x&&!t[y].ch[0])
        {
            t[x].ch[1]=t[y].fa=0;
            push_up(x);
        }
    }
    int ck(int x,int y)
    {
        make_root(x);
        return find_root(y)!=x;
    }
}
signed main()
{
    using namespace LCT;
    read(n);read(m);
    for(int i=1; i<=m; i++)
    {
        read(e[i].u);read(e[i].v);
        read(e[i].a);read(e[i].b);
    }
    sort(e+1,e+1+m,cmp);
    for(int i=1; i<=m; i++)
    {
        int idx=n+i,x=e[i].u,y=e[i].v;
        t[idx].w=e[i].b;
        if(x==y)continue;
        if(ck(x,y))
            link(x,idx),link(idx,y);
        else
        {
            split(x,y);int now=t[y].id;
            if(t[now].mx<=e[i].b)continue;splay(now);
            t[t[now].ch[0]].fa=t[t[now].ch[1]].fa=0;
            link(x,idx);link(idx,y);
        }
        if(!ck(1,n))
        {
            split(1,n);
            ans=min(ans,t[n].mx+e[i].a);
        }
    }
    if(ans==INF)write(-1);
    else write(ans);pc('\n');
    return 0;
}

转载请说明出处

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
P2375 [NOI2014] 动物园是一道经典的动态规划题目,以下是该题的详细题意和解题思路。 【题意描述】 有两个长度为 $n$ 的整数序列 $a$ 和 $b$,你需要从这两个序列中各选出一些数,使得这些数构成一个新的序列 $c$。其中,$c$ 序列中的元素必须在原序列中严格递增。每个元素都有一个价值,你的任务是选出的元素的总价值最大。 【解题思路】 这是一道经典的动态规划题目,可以采用记忆化搜索的方法解决,也可以采用递推的方法解决。 记忆化搜索的代码如下: ```c++ #include <iostream> #include <cstdio> #include <cstring> using namespace std; const int MAXN = 1005; int dp[MAXN][MAXN], a[MAXN], b[MAXN], n; int dfs(int x, int y) { if (dp[x][y] != -1) return dp[x][y]; if (x == n || y == n) return 0; int res = max(dfs(x + 1, y), dfs(x + 1, y + 1)); if (a[x] > b[y]) { res = max(res, dfs(x, y + 1) + b[y]); } return dp[x][y] = res; } int main() { scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", &a[i]); for (int i = 0; i < n; i++) scanf("%d", &b[i]); memset(dp, -1, sizeof(dp)); printf("%d\n", dfs(0, 0)); return 0; } ``` 其中,dp[i][j]表示选到a数组中第i个元素和b数组中第j个元素时的最大价值,-1表示未计算过。dfs(x,y)表示选到a数组中第x个元素和b数组中第y个元素时的最大价值,如果dp[x][y]已经计算过,则直接返回dp[x][y]的值。如果x==n或者y==n,表示已经遍历完一个数组,直接返回0。然后就是状态转移方程了,如果a[x] > b[y],则可以尝试选b[y],递归调用dfs(x, y+1)计算以后的最大价值。否则,只能继续遍历数组a,递归调用dfs(x+1, y)计算最大价值。最后,返回dp[0][0]的值即可。 递推的代码如下: ```c++ #include <iostream> #include <cstdio> #include <cstring> using namespace std; const int MAXN = 1005; int dp[MAXN][MAXN], a[MAXN], b[MAXN], n; int main() { scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", &a[i]); for (int i = 0; i < n; i++) scanf("%d", &b[i]); for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]); if (a[i] > b[j]) { dp[i][j] = max(dp[i][j], dp[i][j + 1] + b[j]); } } } printf("%d\n", dp[0][0]); return 0; } ``` 其中,dp[i][j]表示选到a数组中第i个元素和b数组中第j个元素时的最大价值。从后往前遍历数组a和数组b,依次计算dp[i][j]的值。状态转移方程和记忆化搜索的方法是一样的。 【参考链接】 P2375 [NOI2014] 动物园:https://www.luogu.com.cn/problem/P2375

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值