洛谷 P3588 【[POI2015]PUS】题解

算法分析:

首先,有一个最简单的思路,把这个区间里的其它数向这 k 个数连一条边权为 1 的边,表示这 k 个数要比其它的数大,然后跑一边拓扑排序,求最长路。如果有环,那么问题就无解。否则,有解的图一定是一个有向无环图。

考虑为什么要求最长路。这里的最长路表示该点的最小取值。如果我们求出来的这个点的 dis_i 大于给定数范围的限制,或者当前点是给定的,但我们求出的最长路大于他给定的值,那么就无解,否则直接输出从 1 到 n 的 dis_i​ 即可。

但这道题的边数有 \mid S1 \mid \times \mid S2 \mid 条,S1 是给定的 k 个点,S2 是其余的点。显然炸了。

考虑第一个优化,我们可以建一个虚拟点 node,把 node 向 k 个点连一条边权为 1 的边,然后把其它的区间向虚拟点 node 连一条边权为 0 的边,这样我们就可以表示上述所说的把这个区间里的其它数向这 k 个数连一条边权为 1 的边。

但边的条数我们还是不能接受。

考虑进一步的优化。我们有 m 个区间,每一个区间最多有 k+1 段。跟区间有关的连边,我们可以想到线段树优化建图。具体就是,线段树的两个孩子节点分别向父节点连一条边权为 0 的边,然后区间向虚拟点 node 连边就可以表示为线段树上的一段区间向虚拟点 node 连边。这样向每段区间最多有 \log n 条边,区间的总数是 \sum k,线段树内部有 2n 条边,给定的 k 个点需要连边,这样边的总数是 k + \sum k \log n + 2n,可以过这道题。

最后说一下如何判环,我们发现在拓扑排序中,把每一个点 i 如果访问到就 vis_i = 1 最后看是否有点没有被访问过,如果有的话那就是有环,没有的话直接判最长路即可。

总代码:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#define re register
#define rep(a,b,c)  for(re int a(b) ; a<=c ; ++a)
#define drep(a,b,c) for(re int a(b) ; a>=c ; --a)
using namespace std;
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48) ; ch=getchar();}
    return x*f;
}
const int M = 5e5+10;
const int limit = 1e9;
int a[M],dis[M],head[M],vis[M];
int du[M],t[M];
int n,s,m;
int cnt,tot;
struct edge{
    int to,nxt,w;
}e[M*10]; //注意不要开小
inline void add(int u,int v,int w){ //加边
    e[++cnt].to = v;
    e[cnt].w = w;
    e[cnt].nxt = head[u];
    head[u] = cnt;
    du[v]++;
}
inline void build(int k,int l,int r){ //正常的线段树建立
    if(l == r){
        t[k] = l; //叶节点的编号
        return;
    }
    t[k] = ++tot; //每一个节点的编号
    int mid = (l+r)>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    add(t[k<<1],t[k],0),add(t[k<<1|1],t[k],0); //左右儿子分别向父亲连边
}
inline void link(int k,int l,int r,int x,int y,int node){
    if(x<=l && r<=y){
        add(t[k],node,0); // 这个区间向虚拟点node连边
        return;
    }
    int mid = (l+r)>>1;
    if(x<=mid) link(k<<1,l,mid,x,y,node);
    if(y>mid)  link(k<<1|1,mid+1,r,x,y,node); //正常操作
}
inline void topo(){
    queue<int> q;
    for(re int i(1) ; i<=tot ; ++i){
        if(!dis[i]) dis[i] = 1; //从1开始
        if(du[i] == 0) q.push(i); //把入度为0的点先加进队列
    }
    while(!q.empty()){
        int u = q.front();
        q.pop();
        vis[u] = 1; //标记这个点走到了
        for(re int i(head[u]) ; i ; i=e[i].nxt){
            int v = e[i].to,w = e[i].w;
            if(dis[v] < dis[u] + w) dis[v] = dis[u] + w; //更新最长路
            if(a[v] && dis[v] > a[v]) printf("NIE\n"),exit(0); //如果不满足条件
            du[v]--;
            if(!du[v]) q.push(v);
        }
    }
}
signed main(){
    n=read(),s=read(),m=read();
    tot = n;
    build(1,1,n);
    rep(i,1,s){
        int pos=read(),x=read();
        a[pos] = dis[pos] = x;
    }
    rep(i,1,m){
        int l=read(),r=read(),k=read();
        tot++; //tot就是虚拟点
        int pre = l-1; //上一段区间的左端点
        int x;
        for(re int j(1) ; j<=k ; ++j){
            x = read();
            add(tot,x,1); //虚拟点向x连边
            if(x > pre+1) link(1,1,n,pre+1,x-1,tot); //这个区间没有被连边,就连边
            pre = x; //更新pre
        }
        if(x < r) link(1,1,n,x+1,r,tot); //最后看是否会剩一段区间没有连边
    }
    topo();
    for(re int i(1) ; i<=tot ; ++i) if(!vis[i] || dis[i] > limit) { printf("NIE\n"); return 0; } //判无解
    printf("TAK\n");
    rep(i,1,n) printf("%d ",dis[i]);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值