【CodeForces - 603E】Pastoral Oddities

@Pastoral Oddities@


@题目描述-English@

In the land of Bovinia there are n pastures, but no paths connecting the pastures. Of course, this is a terrible situation, so Kevin Sun is planning to rectify it by constructing m undirected paths connecting pairs of distinct pastures. To make transportation more efficient, he also plans to pave some of these new paths.
Kevin is very particular about certain aspects of path-paving. Since he loves odd numbers, he wants each pasture to have an odd number of paved paths connected to it. Thus we call a paving sunny if each pasture is incident to an odd number of paved paths. He also enjoys short paths more than long paths, so he would like the longest paved path to be as short as possible. After adding each path, Kevin wants to know if a sunny paving exists for the paths of Bovinia, and if at least one does, the minimum possible length of the longest path in such a paving. Note that “longest path” here means maximum-weight edge.

Input
The first line contains two integers n (2 ≤ n ≤ 100 000) and m (1 ≤ m ≤ 300 000), denoting the number of pastures and paths, respectively. The next m lines each contain three integers ai, bi and li, describing the i-th path. The i-th path connects pastures ai and bi (1 ≤ ai, bi ≤ n; ai ≠ bi) and has length li (1 ≤ li ≤ 10^9). Paths are given in the order in which they are constructed.
Output
Output m lines. The i-th line should contain a single integer denoting the minimum possible length of the longest path (maximum-weight edge) in a sunny paving using only the first i paths. If Kevin cannot pave a set of paths so that each pasture is incident to an odd number of paved paths, output  - 1.
Note that the paving is only hypothetical—your answer after adding the i-th path should not be affected by any of your previous answers.

Examples
input
4 4
1 3 4
2 4 8
1 2 2
3 4 3
output
-1
8
8
3

input
3 2
1 2 3
2 3 4
output
-1
-1

input
4 10
2 1 987
3 2 829
4 1 768
4 2 608
3 4 593
3 2 488
4 2 334
2 1 204
1 3 114
1 4 39
output
-1
-1
829
829
768
768
768
488
334
204

Note
For the first sample, these are the paths that Kevin should pave after building the i-th path:
No set of paths works.
Paths 1 (length 4) and 2 (length 8).
Paths 1 (length 4) and 2 (length 8).
Paths 3 (length 2) and 4 (length 3).
In the second sample, there never exists a paving that makes Kevin happy.

@题目大意@

给出n个点与m次加边操作,每个操作加入一条(u,v)长度为l的边。
对于每次操作后,在已经添加的边集中求出一个子集,使得每个点度数均为奇数,且子集的最大边最小。

@性质1@

题目要求的是每个节点的度数为奇数,但是这个东西并不能很好的用数据结构进行高效维护,我们需要对这个条件进行转化。

手算一些数据后,我们发现如果n为奇数,那么永远无法找到一个合法解。更进一步地,如果一个连通块包含奇数个节点,那么这个连通块就无法满足条件的方案。于是我们可以猜测:是不是条件等价于连通块大小的奇偶性?

事实上,的确是这样的:

当且仅当一个连通块包含着偶数个点时,存在一个满足题意的边集子集。

【简要证明一下,可以跳过这一段】:
当一个连通块包含奇数个点时,因为一条边会给两个点的度数+1,整个图的度数+2,所以整个图的度数始终为偶数。假如每个点的度数都为奇数,则整个图的度数就为奇数*奇数=奇数,矛盾。
当一个连通块包含偶数个点时,我们可以这样构造出一个合法方案:先随便找连通块的一个生成树,把它当作有根树。然后从叶子出发,删除它连向父亲的边,再将新增的树中度数为一的点进行迭代处理。(有点儿类似于拓扑排序)对于每一个正在处理的节点,如果它的儿子连它的边中被选中的边有奇数条,则不选它连向它父亲的边,否则就选中它连向它父亲的边。这样就可以保证除根以外的节点是奇数度点。因为整个图的度数为偶数,所以根节点的度数一定也为奇数。

@性质2@

但是,我们已知的数据结构大多是用来维护一棵树的,我们必须还要最优性的分析,删除那些不需要的边。题目要求的是边集的最大边权最小,于是我们可以猜测边集是最小生成树上的边。
事实上,我们又一次的猜对了(被打)

存在一个最优边集,使边集中的边都在最小生成树上。

【以下是作者的自我理解与简要证明】
首先我们需要证明这样一个引理:

将一条路径上的选中的边变成为未选中的边,未选中的边变成为选中的边,起点与终点的奇偶性反转,其他节点的奇偶性不变。

一条路径上除了起点与终点外的点,入边与出边都是成对存在,将入边状态反转,点的奇偶性反转;将出边状态反转,点的奇偶性再次反转,变回原样。而起点有一条出边、终点有一条入边,所以起点与终点的奇偶性将会改变。
特别地,假如路径的起点终点首尾相接,形成一个环的话,会得到这样一个定理:

将一个环上的选中的边变成为未选中的边,未选中的边变成为选中的边,则不会影响方案的合法性。

根据上面这个定理,我们就可以执行这样一个操作:将一个环反转,使环上的最大边变为未选中的边,再删除这条最大边。这个操作是正确的,贪心地想:我们既然可以一直让这条最大边对答案不再有贡献,我们为什么不删掉它呢?

这个操作是不是很像最小生成树的破圈操作呢?
动态维护MST,是不是一道LCT的模板题呢?

@作者最初的做法(可忽略)@

作者最初的做法是这样的:
首先是将边权信息改为点权信息的正常操作。
给每条边一个flag变量表示这条边是否被选中。加入一条边,如果该边连接的两个顶点已经连通,则进行破圈操作,并用链反转操作维护方案的合法性;否则,将边的初始flag置为false,加入该边。
用并查集维护每个连通块的大小,一开始大小为奇数的连通块数量为n,当两个大小为奇数的连通块合并时,奇数连通块的个数减2,当奇数连通块的数量为0时,输出答案。
……
然而并没有办法高效实现链反转操作……该方法作废……

@正确的算法@

上面的方法到底哪里出了问题呢?我们并没有要把方案构造出来,只是问在合法方案下的最大边权。而上面的方法维护出了方案,多走了一步,自然不高效。
考虑这样一个方法:一样的加边破圈维护MST,只是在输出答案前,对最大权值那条边进行检查,如果将这条边cut掉依然存在合法方案,就将它cut掉并继续检测边权第二大的边。这个过程可以用set来进行。怎么判断cut掉是否合法呢?这个时候我们就需要用到性质1。即如果一条边连接的两个连通块的大小都为偶数,这条边就可以被cut掉。运用贪心的思想就不难证明这个方法的正确性。

总结一下算法流程:
1)加入一条边,在set里相应的加入这条边,判断这条边连接的两个节点是否已经连通,连通进行 2),否则进行 3)
2)执行破圈操作,将最大边权的边相应从set里面删除。进行 4)
3)如果加入这条边连接的两个连通块大小为奇数,奇数连通块数量 - 2。进行4)
4)从大到小检测每一条在LCT里的边,如果该边连接的两个连通块大小为偶数,在set与LCT里将这条边删除。进行5)
5)如果奇数连通块的数量为0,输出set里的最大边权,否则输出无解。

@一个实现细节@

怎么用LCT维护一颗子树的大小呢???LCT不是只能维护路径信息吗???

大家可以先看看下面这篇博客的讲解,我觉得很详细了。
dalao的博客——LCT维护子树的方法

我在这里大概讲讲是怎么一回事吧:对于每个节点维护两个信息,即所有虚儿子的信息与总信息。PushUp时更新x的总信息为:x实儿子的总信息+x虚儿子的信息+x本身的信息。

由于Link-Cut-Tree的基本操作中,只有Access和Link会对虚儿子的信息进行修改。所以我们就对这两个操作进行些改变。

Access操作中割断了某条实边,该边变为了虚边,所以应该加到x的虚儿子信息中,加入了某条实边,该边不再是虚边,所以应从x的虚儿子信息中减去。

Link操作中为了在加入x时同时更新y的信息,需要Makeroot(x),Makeroot(y),然后连x->y的虚边,将x的信息统计入y的虚儿子信息中。
【基本全靠复制233因为我觉得这篇博客讲的真的很好】

@代码实现@

如果还有什么不懂的就自行阅读代码吧!代码会告诉你一切的!

#include<set>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 100000;
const int MAXM = 300000;
struct node{
    int key, id, rev;
    int siz1, siz2, s;
    node *fa, *ch[2], *mx;
}tree[MAXN+MAXM+5], *ad[MAXN+MAXM+5];
struct edge{
    int u, v, w, id;
    edge(int _u=0, int _v=0, int _w=0, int _i=0):u(_u), v(_v), w(_w), id(_i){}
}e[MAXM + 5];
bool operator < (edge a, edge b) {
    if( a.w == b.w ) return a.id < b.id;
    return a.w < b.w;
}
set<edge>Set;
set<edge>::iterator it;
node *NIL, *ncnt;
void Init() {
    NIL = ncnt = &tree[0];
    NIL->fa = NIL->ch[0] = NIL->ch[1] = NIL->mx = NIL;
    NIL->key = -1;
}
bool IsRoot(node *x) {
    return (x->fa == NIL) || (x->fa->ch[0] != x && x->fa->ch[1] != x);
}
void SetChild(node *x, node *y, int d) {
    x->ch[d] = y;
    if( y != NIL ) y->fa = x;
}
node *NewNode(int k, int id, int s) {
    ncnt++;
    ncnt->key = k, ncnt->id = id, ncnt->s = ncnt->siz1 = s;
    ncnt->fa = ncnt->ch[0] = ncnt->ch[1] = NIL;
    ncnt->mx = ncnt;
    return ncnt;
}
void PushDown(node *x) {
    if( x->rev ) {
        swap(x->ch[0], x->ch[1]);
        if( x->ch[0] != NIL ) x->ch[0]->rev ^= 1;
        if( x->ch[1] != NIL ) x->ch[1]->rev ^= 1;
        x->rev = 0;
    }
}
void PushUp(node *x) {
    if( x->key > x->ch[0]->mx->key && x->key > x->ch[1]->mx->key )
        x->mx = x;
    else if( x->ch[0]->mx->key > x->ch[1]->mx->key )
        x->mx = x->ch[0]->mx;
    else x->mx = x->ch[1]->mx;
    x->siz1 = x->s + x->ch[0]->siz1 + x->ch[1]->siz1 + x->siz2;
}
void Rotate(node *x) {
    node *y = x->fa;
    PushDown(y), PushDown(x);
    int d = (y->ch[1] == x);
    if( IsRoot(y) ) x->fa = y->fa;
    else SetChild(y->fa, x, y->fa->ch[1] == y);
    SetChild(y, x->ch[!d], d);
    SetChild(x, y, !d);
    PushUp(y);
}
void Splay(node *x) {
    PushDown(x);
    while( !IsRoot(x) ) {
        node *y = x->fa;
        if( IsRoot(y) )
            Rotate(x);
        else {
            if( (y->fa->ch[1] == y) == (y->ch[1] == x) )
                Rotate(y);
            else Rotate(x);
            Rotate(x);
        }
    }
    PushUp(x);
}
void Access(node *x) {
    node *y = NIL;
    while( x != NIL ) {
        Splay(x);
        x->siz2 += x->ch[1]->siz1;
        x->ch[1] = y;
        x->siz2 -= x->ch[1]->siz1;
        PushUp(x);
        y = x, x = x->fa;
    }
}
void MakeRoot(node *x) {
    Access(x), Splay(x);
    x->rev ^= 1;
}
void Link(node *x, node *y) {
    MakeRoot(x); MakeRoot(y);
    x->fa = y; y->siz2 += x->siz1;
}
void Cut(node *x, node *y) {
    MakeRoot(x);
    Access(y), Splay(y);
    y->ch[0] = x->fa = NIL;
    PushUp(y);
}
node *FindRoot(node *x) {
    Access(x), Splay(x);
    node *ret = x;
    while( ret->ch[0] != NIL )
        ret = ret->ch[0];
    PushUp(ret);
    return ret;
}
node *LCA(node *x, node *y) {
    Access(x), Splay(x);
    Splay(y);
    while( y->fa != NIL ) {
        y = y->fa;
        Splay(y);
    }
    return y;
}
node *QueryMAX(node *x, node *y) {
    node *z = LCA(x, y);
    node *ret = z;
    Access(x), Splay(z);
    if( z->ch[1]->mx->key > ret->key )
        ret = z->ch[1]->mx;
    Access(y), Splay(z);
    if( z->ch[1]->mx->key > ret->key )
        ret = z->ch[1]->mx;
    return ret;
}
int n, m, stot;
int main() {
    Init();
    scanf("%d%d", &n, &m);
    for(int i=1;i<=n;i++)
        ad[i] = NewNode(-1, -1, 1);
    stot = n;
    for(int i=1;i<=m;i++) {
        e[i].id = i;
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        if( e[i].u == e[i].v ) continue;
        ad[n+i] = NewNode(e[i].w, i, 0);
        Set.insert(e[i]);
        if( FindRoot(ad[e[i].u]) == FindRoot(ad[e[i].v]) ) {
            node *p = QueryMAX(ad[e[i].u], ad[e[i].v]); Splay(p);
            if( p->key > e[i].w ) {
                Set.erase(e[p->id]);
                Cut(p, ad[e[p->id].u]), Cut(p, ad[e[p->id].v]);
                Link(ad[e[i].u], ad[n+i]), Link(ad[e[i].v], ad[n+i]);
            }
            else Set.erase(e[i]);
        }
        else {
            MakeRoot(ad[e[i].u]), MakeRoot(ad[e[i].v]);
            if( ad[e[i].u]->siz1 % 2 == 1 && ad[e[i].v]->siz1 % 2 == 1 ) stot -= 2;
            Link(ad[e[i].u], ad[n+i]), Link(ad[e[i].v], ad[n+i]);
        }

        if( stot == 0 ) {
            it = Set.end(); it--;
            while( true ) {
                node *p = ad[n+it->id], *q = ad[e[it->id].u], *r = ad[e[it->id].v];
                MakeRoot(p); Access(q); Access(r);
                if( q->siz1 & 1 ) break;
                Access(q);
                if( r->siz1 & 1 ) break;
                Set.erase(it);
                it = Set.end(); it--;
            }
            it = Set.end(); it--;
            printf("%d\n", it->w);
        }
        else printf("-1\n");
    }
}

@END@

就是这样,新的一天里,也请多多关照哦(ノω<。)ノ))☆.。~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值