题目描述
【题目描述】
有一座城市,城市中有N个公交站,公交站之间通过N-1条道路连接,每条道路有相应的长度。保证所有公交站两两之间能够通过一唯一的通路互相达到。
连个公交站之间路径长度定义为两个公交站之间路径上所有边的边权和。
现在要对城市进行规划,将其中M个公交站定为“重要的”。
现在想从中选出K个节点,使得这K个公交站两两之间路径长度总和最小。输出路径长度总和即可(节点编号从1开始)。
【输入格式】
从标准输入读入数据。
第1行包含三个正整数N,M和K分别表示树的节点数,重要的节点数,需要选出的节点数。
第2行包含M个正整数,表示M个重要的节点的节点编号。
接下来N-1行,每行包含三个正整数a,b,c,表示编号为a的节点与编号为b的节点之间有一条权值为c的无向边。每行中相邻两个数之间有一个空格分隔。
【输出格式】
输出到标准输出。
输出只有一行,包含一个整数表示路径长度总和的最小值。
解决思路
①拿到这道题我首先想到的是暴力法求解,但是题目中会有时间限制,这么做会得到正确结果但是没办法拿到满分,之后我想到用图论算法或者是树的算法解决问题,但是也没有思路,最后我想到可以用树形dp解决该问题。
②定义dp[u][i](在u个节点中选择i个重要节点的最佳解,也可以理解为在u个节点下选择i个重要节点对于最终答案的贡献);然后利用深度优先遍历搜索到节点的子树底部并及时更新dp[u][i]数组;在这个过程中我们定义了find(int u,int father)函数深度搜索并更新dp[][]二维数组,同时需要临时Assist数组来保存临时的最小值,具体的dp数组最优解的状态转移方程为:
dp[u][i+j]=min(dp[u][i+j],Assist[i]+dp[son][j]+j(k-j)weight);
③递归向上进行合并,直到还原最初题目要求抽象出来的树为止,最后输出dp[root][k],程序结束。
源代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
public class CCF_17
{
final static int KMAX=100,SIZEMAX=50001;
final static long INF=0x3f3f3f3f;
static SideSet[] SideSet=new SideSet[SIZEMAX];
static int[] Node=new int[SIZEMAX];
static long[] Assist=new long[SIZEMAX];
static long[][] dp=new long[SIZEMAX][KMAX];
static int[] Judge=new int[SIZEMAX];
static int k,n,m;
public static void main(String[] args)
{
for (long[] ll : dp) Arrays.fill(ll,INF);
int root=-1;
Scanner input=new Scanner(System.in);
n=input.nextInt();
m=input.nextInt();
k=input.nextInt();
for (int i=1;i<=m;i++)
{
int side=input.nextInt();
Judge[side] = 1;
}
for (int i=2;i<=n;i++)
{
int side1,side2,weigth;
side1=input.nextInt();
side2=input.nextInt();
weigth=input.nextInt();
root=Math.max(Math.max(root,side1),side2);
if (SideSet[side1]==null)
{
SideSet[side1]=new SideSet();
}
if (SideSet[side2]==null)
{
SideSet[side2]=new SideSet();
}
SideSet[side1].sideList.add(new pair(side2,weigth));
SideSet[side2].sideList.add(new pair(side1,weigth));
}
find(root,0);
System.out.println(dp[root][k]);
}
static void find(int u,int father)
{
dp[u][0] = 0;
if (Judge[u]==1)
{
Node[u]=1;
dp[u][1]=0;
}
for (int n=0;n<SideSet[u].sideList.size();n++)
{
int son=SideSet[u].sideList.get(n).side,weight=SideSet[u].sideList.get(n).weight;
if (son!=father)
{
find(son,u);
int nSum=Math.min(Node[u]+Node[son],k);
int nOther=Math.min(Node[u],k);
for (int j=0;j<=nSum;j++) Assist[j]=dp[u][j];
for (int i=0;i<=nOther;i++)
{
for (int j = 0; j <= nSum - i; j++)
{
dp[u][i+j]=Math.min(dp[u][i+j],Assist[i]+dp[son][j]+j*(k-j)*weight);
}
}
Node[u]+=Node[son];
}
}
}
}
class SideSet
{
ArrayList<pair> sideList=new ArrayList<pair>();
}
class pair
{
int side,weight;
public pair(int side,int weight)
{
this.side=side;
this.weight=weight;
}
}
程序运行结果
总结
这道CCF题目还是相当费时的并且工作量很大。但是我觉得作为计算机学科来说,多多练习这种竞赛题也是颇有裨益的,我能在复习java程序设计语言的同时加强对算法的深入理解。
通过这道题的求解,我重温了树形结构下的动态规划算法。通过将题目抽象建模得到无向树,从而将题目转化成了动态规划问题的求解。