cf366 D. Dima and Trap Graph(枚举、二分、dfs、并查集)

题意:

一张图,每条边有属性 \([l,r]\)。找一条从点1到点n的路径,使路径上所有边的区间的交集最大。

\(n\le 1e3, m\le 3e3,1\le l_i\le r_r\le 1e6\)

思路:

法一:枚举答案的区间左端点,二分找右端点,dfs判断(300ms)

如果在 \(l,r\) 的整个值域 \(1e6\) 上枚举和二分,复杂度是 \(O(m10^6log(10^6))\),会超时。注意到答案区间的左端点一定是某条边的区间左端点,右端点也一定是出现过的某右端点,故只需考虑所有出现过的 \(l/r\) 。复杂度 \(O(mmlogm)\)

const int N = 3e3 + 5;
int n, m, Ls[N], Rs[N];
 
struct node { int v, l, r; };
vector<node> G[N];
 
bool vis[N];
bool dfs(int u, int L, int R)
{
    if(L > R) return 0;
    if(u == n) return 1;
    vis[u] = 1;
    for(auto &[v,l,r] : G[u])
        if(!vis[v] && l <= L && r >= R)
            if(dfs(v, L, R)) return 1;
    return 0;
}

signed main()
{
    iofast;
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int a, b, l, r; cin >> a >> b >> l >> r;
        G[a].pb({b,l,r}), G[b].pb({a,l,r});
        Ls[i] = l, Rs[i] = r;
    }
 
    sort(Ls+1, Ls+1+m), sort(Rs+1, Rs+1+m);
 
    int ans = 0;
    for(int i = 1; i <= m; i++)
    {
        int l = 0, r = m;
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            memset(vis, 0, sizeof vis);
            if(dfs(1, Ls[i], Rs[mid])) l = mid; else r = mid - 1;
        }
        ans = max(ans, Rs[l]-Ls[i]+1);
    }
    if(ans) cout << ans;
    else cout << "Nice work, Dima!";
}

法二:dfs+剪枝(124ms)

剪枝1:如果当前区间长度小于答案就不继续搜了

剪枝2:记录上次搜这个节点的区间 \([lastL,lastR]\),如果现在要搜上次区间的子区间就退出。注意这剪枝是很随便的,只考虑上次的

复杂度玄学,跑得飞快

bool vis[N];
int lastL[N], lastR[N];
void dfs(int u, int L, int R)
{
    if(R-L+1 <= ans) return; //剪枝1
    if(L >= lastL[u] && R <= lastR[u]) return; //剪枝2
    lastL[u] = L, lastR[u] = R;
    if(u == n)
    {
        ans = max(ans, R-L+1);
        return;
    }
    for(auto &[v,l,r] : G[u]) if(!vis[v])
        vis[v] = 1,
        dfs(v, max(l, L), min(r, R)),
        vis[v] = 0;
}

signed main()
{
    iofast;
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int a, b, l, r; cin >> a >> b >> l >> r;
        G[a].pb({b,l,r}), G[b].pb({a,l,r});
    }

    vis[1] = 1;
    dfs(1, 1, 1e6);

    if(ans) cout << ans;
    else cout << "Nice work, Dima!";
}

法三:并查集(93ms)

对所有边按左端点排序。如果答案右端点是 \(r\) ,那么所有左端点小于等于 \(r\),右端点大于等于 \(r\) 的边都能选。把这些边的起点终点合并,看能不能使点1和点n连通。

int n, m, ans;

struct node { int u, v, l, r; };
bool cmp(node a, node b) {return a.l < b.l; }
vector<node> edges;

signed main()
{
    iofast;
    cin >> n >> m;
    while(m--)
    {
        int a, b, l, r; cin >> a >> b >> l >> r;
        edges.pb({a,b,l,r}); //只需存边
    }

    sort(all(edges), cmp);

    //枚举答案右端点
    for(auto [u1,v1,l1,r1] : edges) //其实咋样枚举都无所谓
    {
        for(int i = 1; i <= n; i++) p[i] = i; //每次初始化
        for(auto [u2,v2,l2,r2] : edges)
        {
            if(l2 > r1) break; //没交集
            if(r2 < r1) continue; //不以r1为答案右端点
            mer(u2, v2);
            if(get(1) == get(n))
                ans = max(ans, r1 - l2 + 1);
        }
    }

    if(ans) cout << ans;
    else cout << "Nice work, Dima!";
}

posted @ 2022-03-13 00:22  Bellala  阅读(36)  评论(0)    收藏  举报