DP小题单【3】

树形DP模板
DP小题单【1】
DP小题单【2】

重建道路——树形DP

P1272 重建道路
题意:在树上通过断边的形式,保留 k 个点,问你断开的最小边数。
思路:保留点问题一般都类似于树上背包,先预处理出每个点的子树的点的个数。
定义 f [ i ] [ j ] f[i][j] f[i][j]表示以 i i i为根的子树,保留 j j j 个点需要删掉的最小边数。
而且在 f [ i ] [ j ] f[i][j] f[i][j]中,我们要强制根节点必选。
我们可以初始化 f [ i ] [ 1 ] = 0 f[i][1] = 0 f[i][1]=0 表示以 i i i 为根的子树 保留一个点 ,且不需要删边, 遍历每一个子树的时候我们让 f[u][k] ++ ,表示如果用 j 这个子树更新以 u 为根 含有 k 个节点的状态 不合法的话,说明需要删掉这条边,用其他的子树更新,如果这个子树可以凑出 k 个节点的话,我们这两个值取个 m i n min min,即:
f [ i ] [ k ] = m i n ( f [ i ] [ k ] , f [ j ] [ s ] + f [ i ] [ k − s ] ) , s ∈ ( 1 , k − 1 ) f[i][k] = min(f[i][k],f[j][s]+f[i][k-s]),s∈(1,k-1) f[i][k]=min(f[i][k],f[j][s]+f[i][ks]),s(1,k1)
k − 1 k - 1 k1的原因是根节点必选。
最后我们对每个点都遍历一遍:
对于1号点下边的每个点,我们都可以强制断开与父节点的那条边,让 f [ i ] [ m ] + 1 f[i][m]+1 f[i][m]+1也作为一个答案。
简单的想一想有没有可能一个节点的父节点的那条边会被保留呢 , 比如蓝色节点上边的部分一定就不会作为答案吗?
在这里插入图片描述

就比如 f [ 蓝 色 点 ] [ 6 ] f[蓝色点][6] f[][6]会不会是删掉两条边作为答案呢?
这是肯定的,但是在枚举紫色点的子树情况的时候这个答案已经被包括进去了,所以不需要考虑上边的那个边,直接断开作为解就可以了。

#include<bits/stdc++.h>
using namespace std;
int n , m;
#define int long long
const int N = 700;
int a[N],idx,siz[N];
int ne[N],h[N],e[N],lin[N];
int f[N][N];
void add(int a,int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs1(int u,int father)
{
    siz[u] = 1;
    for(int i = h[u] ;~i ; i = ne[i])
    {
        int j = e[i];
        if(j == father)  continue;
        dfs1(j,u);
        siz[u] += siz[j];
    }
}
void dfs2(int u, int father)
{   
    f[u][1] = 0;
    for(int i = h[u] ; ~i ;i = ne[i])
    {
        int j = e[i];
        if(j == father)  continue;
        dfs2(j,u);
        for(int k = siz[u] ;k >= 1 ; k --)
        {
            f[u][k] += 1;
            for(int s = 1 ; s <= k - 1 ; s ++)
            {
                if(s<=siz[j])
                f[u][k] = min(f[u][k],f[j][s] + f[u][k-s]);
            }
        }
    
    }
}
signed main()
{
    memset(h,-1,sizeof h);
    memset(f,0x3f,sizeof f);
    cin>>n>>m;
    for(int i = 1 ; i <= n - 1 ; i++)
    {
        int a , b;
        cin >> a >> b ;
        add(a , b);
        add(b , a);
    }
    dfs1(1,-1);
    dfs2(1,-1);
    int ans = f[1][m];
    for(int i=2;i<=n;i++)  ans=min(ans,f[i][m]+1);
    cout<<ans<<endl;
}

时态同步

时态同步——树形DP

题意:一颗树,给定一个根节点发出激光,激光会传到各个叶子结点,我们希望激光同时到达所有的叶子结点,所以可以使用道具X。每一个道具X可以使一条边的权值增加 1 ,让你用最少的道具X使得时态同步,求出最少X个数。
思路:如何用改变最少的边权呢?
在这里插入图片描述
比如右边这个子树,因为都比左子树要浅,所以我们可以增加蓝色边 两子树深度之差 的权值,这样比改变右子树内部的边更优。
所以贪心的想,改变浅结点的边更优。
所以我们定义 f [ i ] f[i] f[i]为以 i 为根的子树全部深度一致花费的最少时间,这样在状态转移的时候,因为子树的深度已经统一了,那么这一次的贡献直接就是 d e e p [ u ] − d e e p [ j ] − w [ i ] deep[u] - deep[j] - w[i] deep[u]deep[j]w[i]。deep是每个子树的最大深度。所以,对于这个贡献我们也不需要真的求出 f [ i ] f[i] f[i]了,只累加贡献即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+10;
int f[N],deep[N];
int ne[N],h[N],w[N],idx,e[N],n,m,ans,ct;
void add(int a,int b,int c)
{
	e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
void dfs(int u,int father)
{
    //cout<<u<<endl;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==father)  continue;
        dfs(j,u);
        deep[u]=max(deep[u],deep[j]+w[i]);
    }
}
void dfs2(int u,int father)
{
    //cout<<u<<endl;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==father)  continue;
        dfs2(j,u);
        ans+=deep[u]-w[i]-deep[j];
       // cout<<" ans = "<<ans<<" u = "<<u<<endl;
    }
}
signed main()
{
	memset(h,-1,sizeof h);
    cin>>n>>m;
	for(int i=1;i<=n-1;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	dfs(m,-1);
	dfs2(m,-1);
	cout<<ans<<endl;
}

括号数——树形DP
在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int N = 2e7+10;
#define int long long
char w[N];
int n,m,ne[N],e[N],idx,h[N],sum[N],fa[N],ans;
int f[N];//f代表合法括号数前缀和
int d[N];//d代表当前序列的合法括号数
stack<int> s;
void add(int a,int b)
{
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
	int t=0;
	if(w[u]==')')
	{
		if(!s.empty())
		{
		    t = s.top();
		    f[u]=f[fa[t]]+1;
		    s.pop();	
		}
        
	}
	else    s.push(u);
	sum[u]=sum[fa[u]]+f[u];
	for(int i=h[u];~i;i=ne[i])
	{
		int j = e[i];
        dfs(j);
	}
	if(t)  s.push(t);
	else if(!s.empty())   s.pop();
}
signed main()
{
	memset(h,-1,sizeof h);
    cin>>n;
    for(int i=1;i<=n;i++)
	{
		cin>>w[i];
	}
	for(int i=2;i<=n;i++)
	{
		int x;
		cin>>x;
		add(x,i);
		fa[i]=x;
	}
	dfs(1);    
	for(int i=1;i<=n;i++)
	{
		ans^=(long long)sum[i]*i;
	}
	cout<<ans<<endl;
}

K. Kingdom’s Power——树形DP

题意:
国王拥有无数支军队,并从1号点开始出军,需要占领所有国家,求占领所有国家的总时间。国王可以支配已经出征的军队,也可以随时发出一支新的军队去占领国家。

思路:
树形DP。考虑一个国家被占领的最短时间,一个国家被占领要么是从根节点发了一支军队直接占领,要么是从父节点的军队占领(这支军队不是从根节点发过来的,下图解释这句话)。在这里插入图片描述
假设考虑蓝色节点,如果已经发兵占领了紫色国家。那么蓝色的最短路径是从紫色结点抵达粉色点然后抵达蓝色点。

所以蓝色结点的最优解是父节点的最短距离+1。

再观察绿色结点,假设已经占领了咖啡色结点,它的最短路径是从咖啡色结点回到蓝色结点,再从蓝色点到绿色点。

由此观之,一个点可以从某个叶子结点更新过来。那么绿色点从紫色点更新过来这个状态就相当于父节点的最短距离+1。

所以只需要考虑兄弟结点的叶子结点的更新情况即可。

再看一个图:
在这里插入图片描述
如果仅仅考虑叶子结点更新当前结点显然也不行,比如这个粉色结点的最短距离显然是 min (红色点到当前点的距离,根节点发一支新军队的距离),而不能仅仅是到某个叶子结点的距离。

排序:应该可以发现遍历兄弟结点的叶子结点的时候要尽可能用叶子结点浅的点,所以按每个子树到最远的叶子结点的距离排序,这样可以贪心的取到min。

实现
假设到父节点的总时间(距离)是dis。

定义一个mi=dis作为从父亲节点更新过来的状态,注意,只有第一个子节点的距离是dis+1,其他的节点的距离一定是兄弟结点到叶子结点的距离与deep[u](深度)取最小值,因为这个时间指的是总时间,只有第一个子树可以继承父结点的时间,对于之后的子树来说父节点+1的这个时间不合法了。

之前说的距离其实是总时间,大家注意一下这个问题。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
#define x first
#define y second
const int N=1e6+100;
const int inf=0x3f3f3f3f3f3f3f;
int deep[N],val[N];
vector<pair<int,int>>node[N];//x存子树链的长度,要用这个值排序,y存子树编号 
int dfs1(int u,int dep)
{
    if(node[u].empty())  return 1;//遇到叶子节点,返回链长为 1  
    deep[u]=dep;//dep指的是树的深度,从上到下 
    for(auto &it:node[u])
    {
        int j=it.y;
        it.x=max(it.x,dfs1(j,dep+1));//统计根到当前节点深度的同时,顺便返回当前点到叶子节点的距离(最长) 
    }
    sort(node[u].begin(),node[u].end());//根据子节点到叶子节点的距离排序 
    return node[u].back().x+1;//返回最长的一条链的长度 + 1 , 即当前点到叶子节点的距离 
}

int dfs2(int u,int dis)//dis指的是占领当前点的时间————即答案 
{   //此时dfs返回的是当前点到叶子节点的最短距离——即到最近的那个叶子节点 
    val[u]=dis;//先更新当前点的答案 
    if(node[u].empty())  return 1;//到叶子节点返回 1
    int mi=dis;//mi维护从兄弟节点的叶子点的军队到达当前节点时间,只有第一次是父节点的状态+1
    for(auto &it:node[u])
    {   
        int j=it.y;//先明确dfs返回值的含义,即 mi+1 的含义 
        mi=min(deep[u],dfs2(j,mi+1));//因为从小到大遍历,mi+1一直是当前节点最优的占领时间。 
    } 
    return mi+1; //mi+1指的是这个点的最近的叶子节点——去更新其他兄弟节点的距离(画图数一数) 
}
signed main()
{
    int t;
    scanf("%lld",&t);
    int k=0;
    while(t--)
    {
        int n;
        scanf("%lld",&n);
        for(int i=1;i<=n;i++)  
            node[i].clear();
        for(int i=2;i<=n;i++)
        {
            int fa;
            scanf("%lld",&fa);
            node[fa].push_back({0,i});
        }
        dfs1(1,0);
        dfs2(1,0);
        int ans=0;
        for(int i=1;i<=n;i++)
        {
            if(node[i].empty())  ans+=val[i];
        }
        printf("Case #%lld: %lld\n",++k,ans);
    }           
}

Kamp——树形DP

P6419 [COCI2014-2015#1] Kamp
题意:从1 ~ n号点选出一点开 party ,同时1 ~ n 号点中 有 k 个人分布在其中, 司机确定好开 party的点后,司机要在party结束送每个人回家(一辆车带一个人,且最后送完最后一个人事件结束,不用回到party点)。
这道题和上一道题题面很类似,但是做法不一样,那个最短时间是占领所有国家的最短时间,必须走到叶子结点,而且还可以从根发出一个新的军队去占领,但这道题相当于只有一支军队,来回走,除了最后一个人其他的都走了一个完整的来回。
所以思路也出来了:我们要维护一个最远距离,答案就是送完所有人的时间 - 最远距离。
所有人的时间我们可以通过维护一个根节点上边的最少时间(g[u]) + 下边的最少时间(f[u])来解决。 最远距离就相当于树形DP板子题【树的中心】,用一个dist和up维护出根节点向下和向上的最长路径,当然求up还需要次长距离就不多说了。
最后对于每个点的答案就是 f [ i ] + g [ i ] − m a x ( d i s t [ i ] [ 0 ] , u p [ i ] ) f[i]+g[i]-max(dist[i][0],up[i]) f[i]+g[i]max(dist[i][0],up[i])

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6+10;
int ne[N],h[N],e[N],w[N],n,k,idx;
int a[N];//a存某个人位置
int g[N],f[N],siz[N]; //g代表向上的时间,f代表向下的时间
int dis[N][5];//0代表u的最长链,1代表次长链【在u子树里】
int up[N];//【在u子树外】最长链
void add(int a,int b,int c)
{
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
void dfs1(int u,int father)
{
    siz[u]=a[u];//有人的点才计数
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==father)  continue;
        dfs1(j,u);
        siz[u]+=siz[j];
        if(siz[j])//从子树中有人的位置往上递推时间,没人没必要往下算
        {
            f[u]+=(f[j]+2*w[i]);//下去再回来
            if(dis[u][0]<dis[j][0]+w[i])
            {
                dis[u][1]=dis[u][0],dis[u][0]=dis[j][0]+w[i];
            }
            else if(dis[u][1] < dis[j][0] + w[i])
            dis[u][1] = dis[j][0] + w[i];
        }
    }
}
void dfs2(int u,int father)
{
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==father)  continue;
        if(k-siz[j])//不能全部的人都在j这个子树,相当于此时g[j]=0
        {
            g[j]=g[u]+(f[u]-f[j]);
            if(!siz[j])  g[j]+=2*w[i];
            if(dis[j][0]+w[i]==dis[u][0])
                up[j] = max(up[u],dis[u][1]) +w[i];
            else 
                up[j] = max(up[u],dis[u][0])+ w[i];
        }
        dfs2(j,u);
    }
}
signed main()
{
    memset(h,-1,sizeof h);
    cin>>n>>k;
    for(int i=1;i<=n-1;i++)  
    {
        int a,b,c;
        scanf("%lld %lld %lld",&a,&b,&c);
        add(a, b, c);
        add(b, a, c);
    }
    for(int i=1;i<=k;i++)  
    {
        int x;
        cin>>x;
        a[x]++;
    }
    dfs1(1,-1);
    dfs2(1,-1);
    for(int i=1;i<=n;i++)
    {
        cout<<f[i]+g[i]-max(up[i],dis[i][0])<<endl;
    }
}

P3174 [HAOI2009]毛毛虫

毛毛虫——树形DP

题意如图:让你求一只最大的毛毛虫
在这里插入图片描述
思路:和大部分树形DP一样,首先对于根来说找到毛毛虫的两条腿之后,对于它的子树,再也不需要考虑祖宗分支了,因为如果存在的话已经被祖宗算过了(上边的重建道路有提到)。
我们先想想对于每个点的答案是什么?
对于根节点:
应该是过这个点的两个毛毛虫的腿加起来,再加上邻边 - 2。
对于根节点下边的点:
因为已知不考虑父节点作为腿了,所以答案应该是下边的两条腿加起来,加上邻边 - 2 +1,+1是因为不考虑父节点作为腿了,但是也相当于多了一个邻边。
因为每个虫是两条腿,所以显然要维护最大值和次大值,现在问题就是如何维护这两个值。
以前的最大值和次大值一般都是深度更新,这个深度还要考虑邻边。
所以我们定义 f [ i ] 表 示 以 i 为 根 的 树 中 最 大 的 一 条 腿 f[i]表示以i为根的树中最大的一条腿 f[i]i,对于子节点都是已经带着邻边更新上来的,所以 f [ u ] = m a x ( f [ j ] ) + 1 ( 自 己 ) + s i z − 1 ( 邻 边 ) f[u]=max(f[j]) + 1 (自己)+ siz-1(邻边) f[u]=max(f[j])+1+siz1。这样我们就维护好了一条带邻边的腿,而对于当前的节点u的最大值 d 1 d1 d1和次大值 d 2 d2 d2,只需要遍历 f [ j ] f[j] f[j],找出两个值就可以了。
每一个 s i z − 1 siz - 1 siz1都要和 0 0 0取最大值,取到0可能是只有一个邻边,但不能出现负数。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+10;
int f[N],siz[N],ans,res;//siz存有多少个邻接点
int ne[N],h[N],w[N],idx,e[N],as[N],n,m;
void add(int a,int b)
{
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int dfs(int u,int father)
{
    int siz=0;//统计邻边个数
    int d1=0,d2=0;//最大和次大值
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==father) continue;
        dfs(j,u);
        siz++;
        f[u]=max(f[u],f[j]);
        if(f[j]>d1) 
        {
            d2=d1;
            d1=f[j];
        }
        else if(f[j]>d2)  d2=f[j];
    }
    f[u]+=1+max((int)0,siz-1);
    if(father==-1)  ans=max(ans,d1+d2+1+max((int)0,siz-1-(father==-1)));
    else ans=max(ans,d1+d2+1+max((int)0,siz-2+1));//+1指的是父亲的那条边
    
}
signed main()
{
	memset(h,-1,sizeof h);
    cin>>n>>m;
	while(m--)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
	}
	dfs(1,-1);
	cout<<ans<<endl;
}

P6064 [USACO05JAN]Naptime G

Naptime——环形DP

题意:一天平均分成 N N N ( 3 < = N < = 3830 ) (3<=N<=3830) 3<=N<=3830段,要用其中的 B ( 2 < = B < N ) B(2<=B<N) B2<=B<N段时间睡觉,每一段时间都有一个权值,入睡和醒来都是瞬间的不耗费时间,对于一个连续的一段时间X,X中的第一个时间不计入权值(不计入就是浅睡时间,计数的是熟睡时间),问你 B B B的时间段可以得到的最大熟睡权值和。
一天是一个循环的圈,即时间轴是一个环。
思路:环形DP的板子题。
对于一个环要么像环形石子合并一样展开链成两倍,枚举起点终点,要么强制选择讨论。
这个题如果展开两倍的话时间复杂度是 n 3 n^3 n3,会TLE。
我们发现状态转移的时候只会影响相邻的状态,也就是说1~n这个状态的转移是取决于第一个点的状态的,而第一个点的状态又取决于最后一个点有没有选,所以我们只需要让第一个点的状态讨论出来即可。
如果1号点处于非熟睡状态,也就是要么选了最后一个点但是没选第一个点,要么就是最后一个点和第一个点都没选,要么是没选最后一个点,选了第一个点。
这三种情况都可以包含在 m a x ( f [ n ] [ m ] [ 0 ] , f [ n ] [ m ] [ 1 ] ) max(f[n][m][0],f[n][m][1]) max(f[n][m][0],f[n][m][1])中。
此时要强制一号点处于非熟睡—— f [ 1 ] [ 1 ] [ 1 ] = 0 f[1][1][1] = 0 f[1][1][1]=0
如果1号点处于熟睡状态,那么一定是 f [ n ] [ m ] [ 1 ] f[n][m][1] f[n][m][1]才能到达这个状态。
此时要强制一号点处于熟睡—— f [ 1 ] [ 1 ] [ 1 ] = a [ 1 ] f[1][1][1] = a[1] f[1][1][1]=a[1]

#include <bits/stdc++.h>
using namespace std;
const int N	= 3831;
int a[N];
int f[N][N][2];
int n,m;
signed main() 
{
	cin >> n >> m ;
	for(int i = 1; i <= n ; i ++)
	{
		scanf("%d",&a[i]);
	}
    memset(f,-0x3f,sizeof f);
	for(int i=1;i<=n;i++)  f[i][1][1] = 0;
    for(int i=1;i<=n;i++)  f[i][0][0] = 0;
    for(int i = 2 ; i <= n; i++)
	{
		for(int j = i ; j >= 1 ; j--)
		{
		    f[i][j][0] = max(f[i-1][j][1],f[i-1][j][0]);	
		    f[i][j][1] = max(f[i-1][j-1][1] + a[i],f[i-1][j-1][0]);
		}
	}
	int res = max(f[n][m][0],f[n][m][1]);//如果
    memset(f,-0x3f,sizeof f);
    for(int i=1;i<=n;i++)  
    {
        if(i==1)
        f[i][1][1]=a[1];
        else f[i][1][1]=0;
    }
    for(int i=1;i<=n;i++)  f[i][0][0]=0;
    for(int i = 2; i <= n ; i++)
    {
        for(int j = i ; j >=1 ; j --)
        {
            f[i][j][0] = max(f[i-1][j][1],f[i-1][j][0]);	
		    f[i][j][1] = max(f[i-1][j-1][1] + a[i],f[i-1][j-1][0]);
        }
    }
    res = max(res,f[n][m][1]);
	cout<<res<<endl;
}

P1453 城市环路

城市环路——基环树DP

题意:n 个点 , n 条边 , ,每个点有权值,保证图只有一个单环,任意一条边的两个点不能同时选,最后问你选完所有的点之后 的 s u m w 再 × k 的sum_w再× k sumw×k等于多少, k k k是一个常数。
思路:找环,然后任取环上两个点做一遍DP。和环形DP一样,对于两个点不同时选的处理办法就是,先让一个点强制不选,另一个无所谓,然后再让另一个强制不选,这样的话一定不会出现同时选这种情况,且其他情况都被包括进去了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int ne[N],e[N],h[N],idx;
int st[N],ins[N];
int n,m,fu[N];
double f[N][5],p[N],k;
double ans=0;
void add(int a,int b)
{
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int flag=0;
double dfs(int u,int father,int sign)
{
    f[u][1]=p[u];
    f[u][0]=0;
    for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==father) continue;
		if(j==sign)  continue;
		dfs(j,u,sign);
		f[u][1]+=f[j][0];
		f[u][0]+=max(f[j][0],f[j][1]);
	}
}
void dfs_c(int u,int from)
{
	if(flag)  return ;
	st[u]=ins[u]=1;
	for(int i=h[u];~i;i=ne[i])
	{
		if(i==(from^1))  continue;
		int j=e[i];
        if(!st[j]) dfs_c(j,i);
		else if(ins[j])
		{
			flag=1;
			dfs(j,u,j);
			dfs(u,j,u);
			ans=max(f[j][0],f[u][0]);
		}
	}
}
int main()
{
	memset(h,-1,sizeof h);
    cin>>n;
	for(int i=1;i<=n;i++)  cin>>p[i];
	for(int i=1;i<=n;i++)
	{
        int a,b;
		cin>>a>>b;
		a++;b++;
		add(a,b);
		add(b,a);
	}
	cin>>k;
	
	dfs_c(1,-1);
	
	printf("%.1lf",ans*k);
}

P4381 [IOI2008] Island

岛屿——基环树DP

题意: 给定一个无向图, 可以任从一个岛开始浏览,任何一个岛不可以游览一次以上,当遍历完一个基环树之后,可以跳到另一个基环树上的任意一个点,重复上述过程。
问你经过的总边权的最大值?
在这里插入图片描述
dfs找环再存下来,对每个环求顺时针前缀和,枚举 2 ∗ n 2*n 2n的长度就可以求得当前环的任意两点距离了,对于环上每个点都有 d i s t j dist_j distj,代表以 j j j为子树的结点的最长路径,最后的答案就是 d i s t j + d i s t s + s u m [ s ] − s u m [ j ] dist_j+dist_s+sum[s]-sum[j] distj+dists+sum[s]sum[j],在对这个式子变形为 d i s t j − s u m [ j ] + d i s t s + s u m [ s ] dist_j-sum[j]+dist_s+sum[s] distjsum[j]+dists+sum[s],前边一部分可以用单调队列优化。

#include <iostream>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 1000010, M = 2 * N;

int n;
int h[N], e[M], w[M], ne[M], idx;
int fu[N], fw[N], q[N];
int cnt, cir[M], ed[M]; // cnt表示环数,cir[i]表示所有环中第i个点的编号,ed[i]表示第i个环的截至位置
LL s[N], sum[M], d[M];
bool st[N], ins[N];
LL ans;

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void dfs_c(int u, int from)
{
    st[u] = ins[u] = true;
    for (int i = h[u]; ~i; i = ne[i])
    {
        if (i == (from ^ 1)) continue; // 如果是反向边则跳过,必须用边来判断,这样才能确定是通过反向变回到父节点
        int j = e[i];
        fu[j] = u, fw[j] = w[i];
        if (!st[j]) dfs_c(j, i);
        else if (ins[j]) // 如果遍历到栈中的点,说明形成了环
        {
            cnt++;
            ed[cnt] = ed[cnt - 1];
            LL tot = w[i];
            for (int k = u; k != j; k = fu[k]) // 从当前往回遍历构造新环
            {
                s[k] = tot;
                tot += fw[k];
                cir[++ ed[cnt]] = k;
            }
            s[j] = tot, cir[++ ed[cnt]] = j;
        }
    }

    ins[u] = false;
}

LL dfs_d(int u) // 求以u为根节点的子树的最大深度
{
    st[u] = true;
    LL d0 = 0, d1 = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (st[j]) continue;
        LL d = dfs_d(j) + w[i];
        if (d >= d0) d1 = d0, d0 = d;
        else if (d > d1) d1 = d;
    }
    ans = max(ans, d1 + d0);
    return d0;
}

int main()
{
    ios::sync_with_stdio(0);
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++)
    {
        int a, l;
        cin >> a >> l;
        add(i, a, l), add(a, i, l);
    }

    for (int i = 1; i <= n; i++)
        if (!st[i])
            dfs_c(i, -1);

    memset (st, 0, sizeof st);
    for (int i = 1; i <= ed[cnt]; i++) st[cir[i]] = true; // 将所有环上的点设为不可遍历,为了后面求d[]

    LL res = 0;
    for (int i = 1; i <= cnt; i++) // 遍历所有的环
    {
        ans = 0; // 当前基环树的直径
        int sz = 0; // 当前基环树的环的大小
        for (int j = ed[i - 1] + 1; j <= ed[i]; j++) // 遍历环上的每一个点
        {
            int k = cir[j];
            d[sz] = dfs_d(k); // 求以当前点为根的子树的最大深度
            sum[sz] = s[k];
            sz++;
        }

        // 破环成链,前缀和数组和d[]数组延长一倍
        for (int j = 0; j < sz; j++)
            d[sz + j] = d[j], sum[sz + j] = sum[j] + sum[sz - 1];

        // 做一遍滑动窗口,比较依据是d[k] - sum[k]
        int hh = 0, tt = -1;
        for (int j = 0; j < sz * 2; j++)
        {
            while (hh <= tt && q[hh] <= j - sz) hh++;
            if (hh <= tt) ans = max(ans, d[j] + sum[j] + d[q[hh]] - sum[q[hh]]);
            while (hh <= tt && d[j] - sum[j] >= d[q[tt]] - sum[q[tt]]) tt--;
            q[ ++ tt] = j;
        }
        res += ans;
    }

    cout << res << endl;
}

P2607 [ZJOI2008]骑士

骑士——基环树DP

题意:一些骑士出征,每个骑士都有战斗力也都有与他有矛盾的骑士,在出征的时候选出的骑士不能存在矛盾,而且使得他们的战斗力最大。
思路:就相当于最大独立集,上司的舞会那道题。
多了一个基环树的处理,但是这个题并不只是一个环,所以我们对每个点找环,找到环之后任取两个点做最大独立集,分别强制两个点的不选的情况,一定可以避开同时选,然后对这两个值取 m a x max max即可。

//f[u][0]表示不选这个点的最大独立集
//骑士之间相互仇恨,保证每个联通块内有仅有一个环
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
#define int long long
int root,ans;
int ne[N],e[N],w[N],idx,h[N],n,m,vis[N],fa[N],f[N][5];
void add(int a, int b)  
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
    vis[u]=1;//标记连通块内部的点
    f[u][0]=0,f[u][1]=w[u];
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(j==root)  continue;
        dfs(j);
        f[u][0]+=max(f[j][0],f[j][1]);
        f[u][1]+=f[j][0];
    }
}
void find_circle(int x)
{
    vis[x]=1;
    root=x;
    while(!vis[fa[root]])
    {
        root=fa[root];
        vis[root]=1;
    }
    dfs(root);
    int t=f[root][0];
    root=fa[root];
    dfs(root);
    ans+=max(t,f[root][0]);
}
signed main()
{
    memset(h, -1, sizeof h);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int b;
        cin>>w[i]>>b;
        add(b,i);
        fa[i]=b;
    }
    for(int i=1;i<=n;i++)
    {
        if(!vis[i])
        {
            find_circle(i);
        }
    }
    cout<<ans<<endl;
}

1175. 最大半连通子图

最大半连通子图——tarjan+拓扑排序DP

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10,M=2e6+10;
int n,m,mod;
int h[N],hs[N],e[M],ne[M],idx;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,scc_size[N];
int f[N],g[N];//f代表这个点的最大值,g代表最值方案
void add(int h[],int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
    dfn[u]=low[u]= ++timestamp;
    stk[++top]=u,in_stk[u]=true;
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
        }
        else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
    }
    if(dfn[u]==low[u])
    {
        ++scc_cnt;
        int y;
        do{
            y=stk[top--];
            in_stk[y]=false;
            id[y]=scc_cnt;
            scc_size[scc_cnt]++;
        }while(y!=u);
    }
}
int main()
{
    memset(h,-1,sizeof h);
    memset(hs,-1,sizeof hs);
    scanf("%d %d %d",&n,&m,&mod);
    while(m--)
    {
        int a,b;
        scanf("%d %d",&a,&b);
        add(h,a,b);
    }
    for(int i=1;i<=n;i++) 
        if(!dfn[i])  tarjan(i);
    unordered_set<LL> S;
    for(int i=1;i<=n;i++)
    {
        for(int j=h[i];~j;j=ne[j])
        {
            int k=e[j];
            int a = id[i], b =id[k];
            LL hash = a*1000000ll + b;
            if(a!=b&&!S.count(hash))
            {
                add(hs,a,b);
                S.insert(hash);
            }
        }
    }
    for(int i=scc_cnt;i>=1;i--)
    {
        if(!f[i])
        {
            f[i]=scc_size[i];
            g[i]=1;
        }
        for(int j=hs[i];~j;j=ne[j])
        {
            int k=e[j];
            if(f[k]<f[i]+scc_size[k])
            {
                f[k]=f[i]+scc_size[k];
                g[k]=g[i];
            }
            else if(f[k]==f[i]+scc_size[k])
            {
                g[k]=(g[k]+g[i])%mod;
            }
        }
    }
    int maxf = 0 , sum = 0;
    for(int i=1;i<=scc_cnt;i++)
    {
        if(f[i]>maxf)
        {
            maxf=f[i];
            sum=g[i];
        }
        else if(f[i]==maxf) sum = (sum+g[i])%mod;
    }
    printf("%d\n",maxf);
    printf("%d\n",sum);
    return 0;
}

采蘑菇——tarjan+拓扑排序DP

采蘑菇
to be continued。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值