环上的DP

bzoj1040 骑士
题意:共有n个骑士,每个骑士有一个战斗力w和他最憎恨的人,不能将他和他最憎恨的儿女同时派出去战斗,问能派出去的最大战斗力总和为多少?
解法:显然这是个求最大独立子集的题目。但是这并不是一个树,而是若干基环树组成的森林,为什么这么说。因为两个骑士相互憎恨,等价于一条边,如果存在这种情况,该连通分量就是一棵树,否则就是一个基环树(只有一个环的“树”)
基环树如何求最大独立集?
对环上某点进行讨论:
1)选取该点,那么顺序遍历该环,遍历到该点的前驱时,不能选取,因此只需要删除该前驱即可。
2)不选取该点,前驱可选,直接进行环上dp即可
但是在进行环上dp之前必须先对环上的每棵树进行树形dp求出选取根和不选根的最大权值和

#include<iostream>
#include<cstdio>
#include<string.h>
#include<cmath>
#include<algorithm>
#include<stdlib.h>
#include<vector>
#include<set>
#include<map>
#include<queue>
#define ll long long
#define PLL pair<ll,ll>
#define MP make_pair
#define X first
#define Y second
using namespace std;

const ll maxn = 1000000+10;
ll n,m,head[maxn],tot;
struct node{
    ll v,nxt;
}e[maxn*2];
ll vis[maxn],flag,vis2[maxn];
ll w[maxn];

ll dp[maxn][2];

ll has[maxn],cir[maxn],cnt;
ll nxt[maxn];

ll sumans,ans;
ll inq;
map<PLL,bool> ma;

void add(ll u,ll v){
    e[tot].v=v,e[tot].nxt=head[u],head[u]=tot++;
}

void bianli(ll u,ll f){
    vis2[u]=1;
    for(ll i=head[u];i!=-1;i=e[i].nxt)
        if(e[i].v!=f&&!vis2[e[i].v]) bianli(e[i].v,u);
}

void find_circle(ll u,ll f){//ÕÒµ½»·
    vis[u]=1;
    for(ll i=head[u];i!=-1;i=e[i].nxt){
        ll v=e[i].v;
        if(v==f) continue;
        if(vis[v]) { inq=v,flag=v; return; }
        find_circle(v,u);
        if(flag>0){
            if(flag==u) flag=-1;
            return;
        }
        if(flag==-1) break;
    }
    vis[u]=0;
}

void dfs_circle(ll u,ll f){//±éÀú»·
    if(has[u]) return;
    cir[++cnt]=u;
    has[u]=cnt;
    for(ll i=head[u];i!=-1;i=e[i].nxt){
        ll v=e[i].v;
        if(v==f||!vis[v]) continue;
        nxt[u]=v;
        dfs_circle(v,u);
        break;
    }
}

void dfs(ll u,ll f){
    ll tmp1=0,tmp0=0;
    for(ll i=head[u];i!=-1;i=e[i].nxt){
        ll v=e[i].v;
        if(vis[v]||v==f) continue;
        dfs(v,u);
        tmp1+=dp[v][0],tmp0+=max(dp[v][0],dp[v][1]);
    }
    dp[u][1]=tmp1+w[u],dp[u][0]=tmp0;
}

PLL dfs1(ll u){
    ll tmp1,tmp2;
    if(nxt[u]!=cir[1]){
        PLL a=dfs1(nxt[u]);
        tmp1=dp[u][1]+a.Y;
        tmp2=dp[u][0]+max(a.X,a.Y);
    }
    else tmp1=tmp2=dp[u][0];
    return MP(tmp1,tmp2);
}

PLL dfs2(ll u){
    ll tmp1,tmp2;
    if(nxt[u]!=cir[1]){
        PLL a=dfs2(nxt[u]);
        tmp1=dp[u][1]+a.Y;
        tmp2=dp[u][0]+max(a.X,a.Y);
    }
    else tmp1=dp[u][1],tmp2=dp[u][0];
    return MP(tmp1,tmp2);
}

int main(){
    //freopen("a.txt","r",stdin);
    scanf("%lld",&n);
    memset(head,-1,sizeof(head)); tot=0;
    for(ll i=1;i<=n;i++){
        ll v;
        scanf("%lld%lld",&w[i],&v);
        ll x=min(i,v),y=max(i,v);
        if(ma.find(MP(x,y))!=ma.end()) continue;
        ma[MP(x,y)]=1;
        add(i,v),add(v,i);
    }
    for(ll l=1;l<=n;l++){
        if(!vis2[l]){
            inq=0,cnt=0,flag=0;
            find_circle(l,0);
            if(!inq){
                dfs(l,0);
                ans=max(dp[l][0],dp[l][1]);
                sumans+=ans;
                bianli(l,0);
                continue;
            }
            dfs_circle(inq,0);
            for(ll i=1;i<=cnt;i++){
                ll u=cir[i],tmp0=0,tmp1=0;
                for(ll j=head[u];j!=-1;j=e[j].nxt){
                    ll v=e[j].v;
                    if(!vis[v]) dfs(v,u),tmp0+=max(dp[v][1],dp[v][0]),tmp1+=dp[v][0];
                }
                dp[u][1]=tmp1+w[u],dp[u][0]=tmp0;
            }
            PLL a=dfs1(cir[1]);
            ans=max(a.X,a.Y);
            a=dfs2(cir[1]);
            ans=max(ans,a.Y);
            sumans+=ans;
            bianli(l,0);
        }
    }
    printf("%lld\n",sumans);
    return 0;
}

bzoj2878 迷失游乐园
题意:n个点的无向连通图,边数只能是n/n-1。从任一点出发等可能遍历相邻未访问节点,问最终形成的路径期望长度是多少?
解法:基环树上的概率dp
1)先解决以每个环点为根的树形dp。
先规定方向:环点定义为根节点,树中深度越深,定义为向下;深度越浅,定义为向上。沿环的方向走也定义为向上
down[u]u向下行走的期望长度
down[fa]=ufa(down[u]+<u,fa>)/son[fa]
这个不需要沿着环方向走,因此无需环形dp,可以先求出来。
up[u]表示树上某点向上行走的长度,需要先求出根节点的up值。
求根节点的up值即从某点开始顺向和逆向走一遍环,每次有两种选择,向下走(进入树)/向上走(沿环走),这个可以通过树形dp求出从某点出发的向上走期望长度,总复杂度为O(k^2),k为环上节点个数。
然后就可以在树上进行dp了。
up[u]=<u,fa>+(up[fa]numfa[fa]+down[fa]numson[fa]down[u]<u,fa>)/(numson[fa]+numfa[fa]1)
如果numson[fa]+numfa[fa]==1,那么后面的一项舍去(即fa除了该儿子u没有其他延伸方向)
环上的点numfa(父节点个数)为2,其余节点numfa为1
numson为树上向下走的方向数

#include<iostream>
#include<cstdio>
#include<string.h>
#include<cmath>
#include<algorithm>
#include<stdlib.h>
#include<vector>
#include<set>
#include<queue>
#define ll long long
#define PII pair<int,int>
#define MP make_pair
using namespace std;

const int maxn = 100000+10;
int n,m,head[maxn],tot;
struct node{
    int v,nxt,w;
}e[maxn<<2];
int vis[maxn],flag;

double down[maxn],up[maxn];
int fa[maxn],son[maxn];

int has[maxn],cir[maxn],cnt;
int nxt[maxn],pre[maxn];
int len[50][50];

double ans;

void add(int u,int v,int w){
    e[tot].v=v,e[tot].nxt=head[u],e[tot].w=w,head[u]=tot++;
}

void find_circle(int u,int f){//找到环
    vis[u]=1;
    for(int i=head[u];i!=-1;i=e[i].nxt){
        int v=e[i].v;
        if(v==f) continue;
        if(vis[v]) { flag=v; return; }
        find_circle(v,u);
        if(flag>0){
            if(flag==u) flag=-1;
            return;
        }
        if(flag==-1) break;
    }
    vis[u]=0;
}

void dfs_circle(int u,int f){//遍历环
    if(has[u]) return;
    cir[++cnt]=u;
    has[u]=cnt;
    fa[u]=2;
    for(int i=head[u];i!=-1;i=e[i].nxt){
        int v=e[i].v,w=e[i].w;
        if(v==f||!vis[v]) continue;
        pre[v]=u,nxt[u]=v;
        dfs_circle(v,u);
        len[has[u]][has[v]]=len[has[v]][has[u]]=w;
        break;
    }
}

void dfs_down(int u,int f){
    for(int i=head[u];i!=-1;i=e[i].nxt){
        int v=e[i].v,w=e[i].w;
        if(v==f||vis[v]) continue;
        fa[v]=1;
        dfs_down(v,u);
        son[u]++;
        down[u]+=(down[v]+w);
    }
    if(son[u]) down[u]/=son[u];
}

void dfs_up(int u,int f,int w){
    up[u]=w;
    if(fa[f]+son[f]>1) up[u]+=((up[f]*fa[f]+down[f]*son[f]-down[u]-w)/(fa[f]+son[f]-1.0));
   // printf("%d %d %.2lf\n",u,f,fa[f]+son[f]-1.0);
    for(int i=head[u];i!=-1;i=e[i].nxt)
        if(e[i].v!=f) dfs_up(e[i].v,u,e[i].w);
}

int main(){
    //freopen("a.txt","r",stdin);
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head)); tot=0;
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w),add(v,u,w);
    }
    find_circle(1,0);
    if(m<n){
        dfs_down(1,0);
        for(int i=head[1];i!=-1;i=e[i].nxt){
            dfs_up(e[i].v,1,e[i].w);
        }
    }
    else{
        for(int i=1;i<=n;i++){
            if(vis[i]){
                dfs_circle(i,0);
                break;
            }
        }
        for(int i=1;i<=cnt;i++) dfs_down(cir[i],0);
        for(int i=1;i<=cnt;i++){
            int u=cir[i];
            double k=1.0;
            for(int j=nxt[u];j!=u;j=nxt[j]){
                if(nxt[j]!=u) up[u]+=k*(len[has[pre[j]]][has[j]]+down[j]*son[j]/(son[j]+1.0));
                else up[u]+=k*(len[has[pre[j]]][has[j]]+down[j]);
                k/=(son[j]+1);
            }
            k=1.0;
            for(int j=pre[u];j!=u;j=pre[j]){
                if(pre[j]!=u) up[u]+=k*(len[has[nxt[j]]][has[j]]+down[j]*son[j]/(son[j]+1.0));
                else up[u]+=k*(len[has[nxt[j]]][has[j]]+down[j]);
                k/=(son[j]+1);
            }
            up[u]/=2.0;
        }
        for(int i=1;i<=cnt;i++){
            int u=cir[i];
            for(int j=head[u];j!=-1;j=e[j].nxt){
                int v=e[j].v,w=e[j].w;
                if(!has[v]) dfs_up(v,u,w);
            }
        }
    }
    for(int i=1;i<=n;i++){
        ans+=(down[i]*son[i]+up[i]*fa[i])/(double)(son[i]+fa[i]);
    }
    printf("%.5lf\n",ans/n);
    return 0;
}

bzoj1023 cactus仙人掌图
题意:定义仙人掌图是每条边最多在一个简单环中的无向连通图。仙人掌图中的直径为任意两点最短路径的最大值。求仙人掌图的直径
解法:
1)先求仙人掌图的dfs生成树
这里需要知道一个结论:dfs生成树的非树边只能从u连向它的祖先
因此每个环在dfs树中一定存在一个最高点
2)定义F[u]:u到以u为根的子树任意一点的最大距离。边集不仅包含树边,还包含最高点属于u的子树的非树边,但是不包括同一环上的边。
对于非环上最高点:
F[u]=max(F[v]+1),vuuv
如果直径不包含环边:
ans=max(ans,uF+2
如果直径包含环边
ans=max(ans,F[i]+F[j]+dis(i,j))ij
这个式子需要用到增倍和单调队列优化两个技巧。
dis(i,j)=min(size,size-(j-i)),为了去掉min符号,需要将元素增倍,然后线性处理:对于每个i求比他小不大于size/2的区间内求F[j]-j的最小值——滑动窗口求最值问题,可以用单调队列优化。
对于每个环的最高点u,在更新完u所在子树的F值和ans值后,F[u]的求法发生变化,需要添加包含环边的路径:
F[u]=max(F[u],dis(u,i)+F[i])iu
将所有的F值和ans值更新完毕后,即可得到正确结果。

#include<iostream>
#include<cstdio>
#include<string.h>
#include<cmath>
#include<algorithm>
#include<stdlib.h>
#include<vector>
#include<set>
#include<map>
#include<queue>
#define ll long long
#define PLL pair<ll,ll>
#define MP make_pair
#define X first
#define Y second
#pragma comment(linker, "/STACK:102400000,102400000")
using namespace std;

const int maxn = 50000+10;
const int maxm = 20000000+10;
int n,m;
struct node{
    int v,nxt;
}e[maxm];
int head[maxn],tot;
void add(int u,int v){ e[++tot].v=v,e[tot].nxt=head[u],head[u]=tot; }
int f[maxn];
int fa[maxn],dep[maxn],dfn[maxn],low[maxn],dfs_clock;
int a[2*maxn],q[2*maxn];
int ans;

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*10+ch-'0';ch=getchar();}
    return x*f;
}

void dp(int root,int u){
    int siz=dep[u]-dep[root]+1;
    int tmp=siz;
    for(int i=u;i!=root;i=fa[i]) a[tmp--]=f[i];
    a[tmp]=f[root];
    for(int i=siz+1;i<=2*siz;i++) a[i]=a[i-siz];
    int top=1,tail=1; q[1]=1;
    int len=siz/2;
    for(int i=2;i<=2*siz;i++){
        while(top<=tail&&i-q[top]>len) top++;
        ans=max(ans,a[i]+i+a[q[top]]-q[top]);
        while(top<=tail&&a[q[tail]]-q[tail]<=a[i]-i) tail--;
        q[++tail]=i;
    }
    for(int i=2;i<=siz;i++) f[root]=max(f[root],a[i]+min(i-1,siz-(i-1)));
}

void tarjan(int u){
    low[u]=dfn[u]=++dfs_clock;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].v;
        if(fa[u]==v) continue;
        if(!dfn[v]){
            fa[v]=u,dep[v]=dep[u]+1;
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else low[u]=min(low[u],dfn[v]);
        if(low[v]>dfn[u]){//u和v不在同一个环上
            ans=max(ans,f[u]+f[v]+1);
            f[u]=max(f[u],f[v]+1);
        }
    }
    for(int i=head[u];i;i=e[i].nxt){//如果有环,那么u是最高点,v是最低点
        int v=e[i].v;
        if(fa[v]!=u&&dfn[v]>dfn[u]) dp(u,v);//与u相连&&dfn比u大&&不是u的儿子=>是u的环上最低点
    }
}

int main(){
    //freopen("a.txt","r",stdin);
    n=read(),m=read();
    while(m--){
        int a,b,k;
        k=read(); a=read(); k--;
        while(k--){
            b=read();
            add(a,b),add(b,a);a=b;
        }
    }
    tarjan(1);
    printf("%d\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值