时隔多日,俺又回来啦!!!
之前一段时间看到代码有点犯恶。。。不过现在没事儿了,所以捡起信心和勇气,继续写下去
最近开始准备蓝桥杯了,所以写点相关的题目吧
不出所料。。刚写一道就挡住了-_--------行,我还是多学一点吧。。。
看了整整一周,终于看明白了(。。。嚎啕大哭。。。)
下面整理一下蒟蒻的思路:
树的直径
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
现在请你找到树中的一条最长路径。
换句话说,要找到一条路径,使得使得路径两端的点的距离最远。
注意:路径中可以只包含一个点。
输入格式
第一行包含整数 n。接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。
输出格式
输出一个整数,表示树的最长路径的长度。数据范围
1≤n≤10000,
1≤ai,bi≤n,
−105≤ci≤105
输入样例:
6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7
输出样例:
22
思路:首先存的图是双向的,因此用链式前向星浅浅存一个双向图
于是构建了下面这个图,然后dfs开搜
可以看出,当搜到经过6号点的路径时,会有以下三种走法:(以2为起点)
1、2—6:直接进行递归以6为根节点的子树,找出以6为根子树最长的路径长度max1即可
2、2—6—3:在处理第一种情况时找出路径长度的次大值max2,而max1+max2即为所求
3、2—6—1:此情况为节点6的第一和第二种情况,让6号节点处理
嘿嘿,所以就有了下面的状态转移方程:
放代码啦:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int head[N],cnt;
int ans;
int f[N];
struct ee
{
int to,from,val;
}edge[N<<1];
void add(int u,int v,int w)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=w;
edge[cnt].from=head[u];
head[u]=cnt;
}//链式前向星建图
int dfs(int root,int fa)
{//自下而上遍历
int max1=0,max2=0;
for(int i=head[root];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
int d=dfs(v,root)+w;
if(d>max1)
{//传递
max2=max1;
max1=d;
}
else if(d>max2)
max2=d;//传递
}
ans=max(ans,max1+max2);
f[root]=max1;
return f[root];
}
int main()
{
cin>>n;
for(int i=1;i<=n-1;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);//双向建边
add(v,u,w);
}
dfs(1,0);//从根节点开始搜
cout<<ans<<endl;
}
树的直径plus(输出路径)
如何找到最长路径呢?
可以设一个pos,代表最长链的根节点,在记录时,可以记录每个点到最底端的最长和次长路径上的下一个节点,这样就可以还原出经过pos节点的最长链的路径,将两边合并即可得到最长链的路径
放代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int cnt,head[N];
int next[N][2];
int ans;
int f[N];
int pos;
struct ee
{
int to,from,val;
}edge[N<<1];
void add(int u,int v,int w)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=w;
edge[cnt].from=head[u];
head[u]=cnt;
}
int dfs(int root,int fa)
{//由下向上遍历
int max1=0,max2=0;
for(int i=head[root];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
int d=dfs(v,root)+w;
if(d>max1)
{
max2=max1;
max1=d;
next[root][1]=next[root][0];
next[root][0]=v;
}//next[root][0]为最长链的下一个点
else if(d>max2)
{
max2=d;
next[root][1]=v;
}
}
if(ans<max1+max2)
{
ans=max1+max2;
pos=root;
}
//若有更长链,则更改ans和pos
//pos两边路径成为最长链和次长链
f[root]=max1;
return f[root];
}
int main()
{
cin>>n;
for(int i=1;i<=n-1;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1,0);
cout<<ans<<endl;
int x=next[pos][0];
stack<int> st;
while(x)
{
st.push(x);
x=next[x][0];
}
while(st.size())
{
cout<<st.top()<<" ";
st.pop();
}
cout<<pos<<" ";
x=next[pos][1];
while(x)
{
cout<<x<<" ";
x=next[x][1];
}
return 0;
}
树的中心(换根dp)
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。
输入格式
第一行包含整数 n。接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。
输出格式
输出一个整数,表示所求点到树中其他结点的最远距离。数据范围
1≤n≤10000,
1≤ai,bi≤n,
1≤ci≤105
输入样例:
5
2 1 1
3 2 1
4 3 1
5 1 1
输出样例:
2
知道了树的中心的定义,于是有了以下思路:(很巧妙!!!)
首先把任意节点当作根节点。
对于一个节点来说,到其他节点可以有下面几种情况:
1、从该节点向下走,到底端的最远距离为up[x]
2、从该节点往上走,到其他节点的最远距离为down[x]
这两种情况取max得到的是到其他节点的最远距离
对于第一种情况:
直接往下递归维护最大值,根据子节点传回来的答案更新父节点
int dfsd(int root,int fa)
{//从上往下递归,在回溯的时候由下往上更新
for(int i=head[root];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
int d=dfsd(v,root);
if(d+w>d1[root])
{
d2[root]=d1[root];
d1[root]=d+w;
next[root]=v;
}
else if(d+w>d2[root])
d2[root]=d+w;
}
return d1[root];
}
对于第二种情况:
从上向下遍历所有点,对于节点u,遍历它的所有子节点v:
- 如果v不在节点u的底端的最长距离路径上,就取到底端距离的最大值和up[u]的最大值加上u到v的距离得到up[v]
- 否则就用次长距离d2[u]和up[u]的最大值加上u到v的距离得到up[v]
- 一直递归到子节点v
代码:
int dfsup(int root,int fa)
{
for(int i=head[root];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
if(next[root]==v)
up[v]=max(up[v],max(d2[root],up[root])+w);
else
up[v]=max(up[v],max(d1[root],up[root])+w);
dfsup(v,root);
}
}
对于一些细节:
假设当前点为 x,其父节点为 fa。
对于点x先往上走,到其他节点的最远距离 up[x] 需要由父节点fa的该状态 up[fa] 来更新:
用 fa先往上走,到其他节点的最远距离up[fa] 和 fa往下走到底端,且不经过当前点x的最远距离 取max + 边长w 来更新 当前点x先往上走,到其他节点的最远距离up[x]。
但是如何得到 fa往下走到底端,且不经过当前点x的最远距离 呢?
我们在第一种情况的 dfs 中已经维护了 fa 往下走到底端的最远距离 down[x],如果点 x 不在这个路径中,那么第一种情况中所维护的 down[x] 就是满足的;
但是如果点 x 在这个路径中,就需要找到 fa 往下走到底端的‘次远距离’。 (同样注意,这个 ‘次远距离’ 并不是真正的从 fa 到最底端的次远,而是再从 x 的其他兄弟节点中更新,所找到的最远距离。)
于是,就需要像上半部分求《树的直径》的递归一样,分别记录 最远距离d1[i] 和 次短距离d2[i]。同时用 ne[fa] 记录
fa 到底端的最远距离是用哪个子节点更新过来的,用于判断子节点 x 是否在最远距离的路径中
最后:
对于一个点x,向上走到其他点的最远距离up[x] 和 向下走到其他点的最远距离d1[i] 取max,便是该点到其他所有点的最远距离。
遍历所有点,取最远距离的最小值 便是 中心点到其他所有点最远距离的最小值。
for(int i=1;i<=n;i++)
ans=min(ans,max(up[i],d1[i]));
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int head[N],cnt;
int d1[N],d2[N];
int next[N];
int up[N];
struct ee
{
int to,from,val;
}edge[N<<1];
void add(int u,int v,int w)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=w;
edge[cnt].from=head[u];
head[u]=cnt;
}
int dfsd(int root,int fa)
{//从上往下递归,在回溯的时候由下往上更新
for(int i=head[root];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
int d=dfsd(v,root);
if(d+w>d1[root])
{
d2[root]=d1[root];
d1[root]=d+w;
next[root]=v;
}
else if(d+w>d2[root])
d2[root]=d+w;
}
return d1[root];
}
int dfsup(int root,int fa)
{//从上往下递归,从上往下更新
for(int i=head[root];i;i=edge[i].from)
{//遍历以root为根的所有子节点
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
if(next[root]==v)
up[v]=max(up[v],max(d2[root],up[root])+w);
else
up[v]=max(up[v],max(d1[root],up[root])+w);
dfsup(v,root);
}
}
int main()
{
cin>>n;
for(int i=1;i<=n-1;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfsd(1,0);
dfsup(1,0);
int ans=1e9;
for(int i=1;i<=n;i++)
ans=min(ans,max(up[i],d1[i]));
cout<<ans<<endl;
}
写得太好了!!!
------摘自经典问题《树的直径》与《树的中心》,详解。-CSDN博客
树形背包
这类题目有个大致的思路:
首先根据题述所给关系构建一个树,然后对每个点进行遍历,再结合背包算法得出状态转移方程即可
((感觉很水)xsbb)
树形dp&状态机
首先要明确状态的变化,确定状态机模型,然后用集合形式将状态表示出来,并定义其属性。接着进行状态计算得出状态转移方程。。。总之很难。。。
P2015 二叉苹果树
现在看来,这题其实也不是很难,属于肥肠水的模版啦
题目中可以看出有一个隐含条件,根据常识,一根树枝被保留下来时,那么从根节点到该节点的所有边都将会被保留下来,因此我们可以设dp[i][j]表示到根节点i节点之间保留了j根树枝时,留下的最多苹果
因此我们就可以得到这个方程了:
当然这题的精髓在于f[u][i-j-1]这个式子,i-j代表了除了保留这个树枝之外所保留的其他树枝(如上文),i-j-1代表u到v之间的这根树枝以及根节点到u节点之间的树枝
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1055;
int n,q;
int cnt;
int head[N];
int dp[N][N];
struct ee
{
int to,from,val;
}edge[N<<1];
void add(int u,int v,int w)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=w;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int r,int fa)
{
for(int i=head[r];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
dfs(v,r);
for(int j=q;j>=1;j--)
for(int k=j-1;k>=0;k--)
dp[r][j]=max(dp[r][j-k-1]+dp[v][k]+w,dp[r][j]);
}
}
int main()
{
cin>>n>>q;
for(int i=1;i<=n-1;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1,0);
cout<<dp[1][q]<<endl;
}
P1352 没有上司的舞会
很简单的一题了
f[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值
f[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值
则 (y是x的儿子)
(y是x的儿子)
先找到唯一的树根root,然后开搜,显而易见最后答案一定是root参加舞会和不参加的f值的max
即ans=max(f[root][0],f[root][1])
#include<bits/stdc++.h>
using namespace std;
const int N=6e3+5;
int n;
int val[N];
int zi[N];
int dp[N][2];
int cnt;
int head[N];
struct ee
{
int to,from;
}edge[N];
void add(int u,int v)
{
cnt++;
edge[cnt].to=v;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int r)
{
dp[r][0]=0;
dp[r][1]=val[r];
for(int i=head[r];i;i=edge[i].from)
{
int v=edge[i].to;
dfs(v);
dp[r][0]+=max(dp[v][1],dp[v][0]);
dp[r][1]+=dp[v][0];
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>val[i];
for(int i=1;i<=n-1;i++)
{
int k,l;
cin>>l>>k;//k是l的上司
add(k,l);//以k为根,l为子
zi[l]=1;//标记是子的节点,下面找出最上根
}
int root;
for(int i=1;i<=n;i++)
if(!zi[i])
{
root=i;
break;
}
dfs(root);
cout<<max(dp[root][0],dp[root][1])<<endl;
}
P8602 [蓝桥杯 2013 省 A] 大臣的旅费
所有的故事
源于这一题
对,没错,我就是写这题的时候被挡住了然后去学树形dp,然后回来一看发现不难了。。。
这题就是树的直径,找到最长路,然后直接代公式~~(题目推出来的):相当于从第0公里走到第max公里。。。
不过因为路是连着走的,所以需要设两个数组s1和s2来维护最大和次大,最后俩加起来就ok了
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
int cnt;
int head[N];
int ans;
int s1[N],s2[N];
struct ee
{
int to,from,val;
}edge[N<<1];
void add(int u,int v,int w)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=w;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int r,int fa)
{
for(int i=head[r];i;i=edge[i].from)
{
int v=edge[i].to;
int w=edge[i].val;
if(v==fa)
continue;
dfs(v,r);
if(s1[v]+w>s1[r])
{
s2[r]=s1[r];
s1[r]=s1[v]+w;
}
else if(s1[v]+w>s2[r])
s2[r]=s1[v]+w;
}
ans=max(ans,s1[r]+s2[r]);
}
int main()
{
cin>>n;
for(int i=1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1,0);
cout<<ans*10+ans*(ans+1)/2<<endl;
}
P2016 战略游戏
这题是一个dp,我们发现,当一个节放了士兵,那么下一个放和不放就无所谓,可若该节点不放,那么下一个节点就必须要放,因此有了状态转移方程:
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1505;
int n;
int k;
int dp[N][2];
int cnt,head[N];
struct ee
{
int to,from;
}edge[N<<1];
void add(int u,int v)
{
cnt++;
edge[cnt].to=v;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int r,int fa)
{
dp[r][0]=0,dp[r][1]=1;
for(int i=head[r];i;i=edge[i].from)
{
int v=edge[i].to;
if(v==fa)
continue;
dfs(v,r);
dp[r][0]+=dp[v][1];
dp[r][1]+=min(dp[v][1],dp[v][0]);
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int u,v;
cin>>u>>k;
for(int j=1;j<=k;j++)
{
cin>>v;
add(u,v);
add(v,u);
}
}
dfs(0,-1);
// for(int i=1;i<=n;i++)
// {
// for(int j=0;j<=1;j++)
// cout<<dp[i][j]<<" ";
// cout<<endl;
// }
cout<<min(dp[0][1],dp[0][0])<<endl;
}
P2018 消息传递
由题可知,想要传到所有人的时间最短,考虑到如果从任一点为根,向子节点传递时间较少的那个儿子先传过去,那么就会出现这边子节点传递完但是另外一边还没有传递完的情况,不难看出这是极其耗时的,效率很低。所以为了提高效率,让时间减少,我们应该从根节点向传递时间较多的子节点先传,然后耗时少的后传---这就是总体思路
具体一点:首先我们可以明确,每个节点都是可以作为根节点的,所以先将所有节点遍历一遍,对每个节点作为根节点从上往下搜,然后设置son数组,用kk计数,代表这一条路上有kk个子节点,son[kk]代表从最底端的节点往上传到第kk个节点消耗的时间,dp[v]代表根节点传到节点v所需要消耗的时间,于是有方程
表示从最底端向上回溯到第kk个节点,将从下面节点传到该节点的最大时间传给该节点
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
int n;
int dp[N];
int minn=1e9;
int head[N];
int ans[N];
int maxx;
int cnt;
struct ee
{
int to,from;
}edge[N<<1];
void add(int u,int v)
{
cnt++;
edge[cnt].to=v;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int r,int fa)
{
int kk=0;
int son[N];
for(int i=head[r];i;i=edge[i].from)
{
int v=edge[i].to;
if(v==fa)
continue;
dfs(v,r);
kk++;
son[kk]=dp[v];
//遍历到第kk个儿子,将它的儿子的最大时间给它
}
sort(son+1,son+kk+1,greater<int>());
for(int i=1;i<=kk;i++)
dp[r]=max(dp[r],son[i]+i);
//因为由大到小排序,所以son[i]+i即为子节点将信息传到根节点的儿子的时间
}
int main()
{
cin>>n;
for(int i=2;i<=n;i++)
{
int v;
cin>>v;
add(i,v);
add(v,i);
}
for(int i=1;i<=n;i++)
{
memset(dp,0,sizeof(dp));
dfs(i,0);
ans[i]=dp[i];
minn=min(minn,dp[i]);
}
cout<<minn+1<<endl;
for(int i=1;i<=n;i++)
if(ans[i]==minn)
cout<<i<<" ";
return 0;
}
P2014 [CTSC1997] 选课
这题是一个很美妙的dp,它不仅有树还有背包,很巧妙的说。。
但也不是很难,很容易得到背包的状态转移方程:
表示:选择u科目,不选或选k个它的子节点v科目的附属科目所能得到的最大学分
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=305;
int n,m;
int cnt;
int dp[N][N];
int head[N];
struct e
{
int to,from;
}edge[N<<1];
void add(int u,int v)
{
cnt++;
edge[cnt].to=v;
edge[cnt].from=head[u];
head[u]=cnt;
}
void dfs(int u)
{
for(int i=head[u];i;i=edge[i].from)
{
int v=edge[i].to;
dfs(v);
for(int j=m+1;j>=0;j--)
for(int k=j-1;k>=0;k--)
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int k;
cin>>k>>dp[i][1];
add(k,i);//必学到附属学科
}
dfs(0);
cout<<dp[0][m+1]<<endl;
}
P2986 [USACO10MAR] Great Cow Gathering G
这题我觉得很水,不过很新颖,怎么说?
首先能想到的思路很简单,就是枚举每个节点作为根节点,将其当做要选择的点,计算其他点的奶牛到这个点的不方便度,最后找到最小值
不过这个思路有缺陷的,复杂度是O(n^2),过不去
看了题解后就明白了
我们先以1为根节点遍历一遍,将子节点的牛一点一点移到根节点,并且统计每一步的奶牛数,然后在第二个dfs里假设所有奶牛都到了1节点,从上往下遍历每个节点,按照原本路径(假设路径为u->v),u节点的奶牛移动到v节点,减去原本v节点的奶牛到u节点的不方便度,再加上u节点到v节点的不方便度(因为所有奶牛要到v节点),最后遍历每个点,将最小值加上原本到根节点1的不方便度即为解
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+5;
ll n;
ll sum;
ll cnt;
ll ans=1e9;
ll head[N];
ll f[N];
ll q[N];
ll c[N];
ll num[N];
ll dis[N];
struct e
{
ll to,from,val;
}edge[N<<1];
void add(ll u,ll v,ll l)
{
cnt++;
edge[cnt].to=v;
edge[cnt].val=l;
edge[cnt].from=head[u];
head[u]=cnt;
}
ll dfs1(ll u,ll fa)
{
ll t=0;
for(ll i=head[u];i;i=edge[i].from)
{
ll v=edge[i].to;
ll w=edge[i].val;
if(v==fa)
continue;
ll s=dfs1(v,u);//子树上的牛的数量
dis[u]+=dis[v]+s*w;
t+=s;
}
q[u]=t+c[u];
return q[u];
}
void dfs2(ll u,ll fa)
{
for(ll i=head[u];i;i=edge[i].from)
{
ll v=edge[i].to;
ll w=edge[i].val;
if(v==fa)
continue;
f[v]=f[u]-q[v]*w+(sum-q[v])*w;
dfs2(v,u);
}
}
int main()
{
cin>>n;
for(ll i=1;i<=n;i++)
{
cin>>c[i];
sum+=c[i];
}
for(ll i=1;i<=n-1;i++)
{
ll u,v,l;
cin>>u>>v>>l;
add(u,v,l);
add(v,u,l);
}
dfs1(1,0);
dfs2(1,0);
for(ll i=1;i<=n;i++)
ans=min(ans,f[i]);
cout<<ans+dis[1]<<endl;
}
资料:
AcWing 1073. 树的中心【树形DP+换根DP】 - AcWing
【动态规划】树形DP完全详解! - Koshkaaa - 博客园 (cnblogs.com)
后续还是会写关于树形dp的题目的,这篇博客主要是用来总结一下这一周的成果,总之加油!!!