题目
题目可以在CSP官网中查看到哟!
结果
如下代码已经100分通过,截图如下:
算法分析
N个酒店之间总共有(N-1)条道路,并且是一个树,因此需要建图来存储树的信息。小编利用vector数组记录树的信息。
题目的要求是要使得所有酒店的等待时间的最大值最小。首先求解最大值最小,很显然是使用二分法的,而我们最需要考虑的就是怎么求解所有酒店的等待时间的最大值。我们先通过一个图来更加直观地展现所有酒店的等待时间的最大值,如下:
上图代表的是对于一种食材的需求情况,蓝色的点代表一个检查站,运输该食材的车从这里出发;黑色的点代表需要该食材的酒店,节点之间的直线就是路线;橙色的点代表车最后一个运送食材的酒店。而所有酒店的等待时间的最大值就是车从蓝色的点出发,给所有黑色的点送完食材后,最后到达橙色的点的时间,该车的路线图如下:
红色的线就是该车需要行驶的路线图,而行驶红色路线的所有时间的总和就是所有酒店的等待时间的最大值。我们可以从如上的路线图中看出时间总和的计算方式,就是:树中所有路的长度的两倍-(从出发点到最后一个酒店的距离)。在如上的式子中,树中所有路的长度的两倍这个数值是固定的,能变化的就是“从出发点到最后一个酒店的距离”这个数值。所以要使所有酒店的等待时间的最大值最小就是使得从出发点到最后一个酒店的距离这个数值最大,因此对于每一个出发点,所有酒店的等待时间的最大值最小就是:树中所有路的长度的两倍-(离出发点最远的所处叶子节点的酒店的距离),对于这个数值,小编是利用dfs深度优先搜索进行求解,详见代码。
题目要求在所有酒店的节点中选取不超过M个节点当作是检查站(也是运输一种食材的出发点),因此我们在预处理节点需要求解并记录每一个节点作为每一种食材的出发点的所有酒店的等待时间的最大值,以便后面的过程使用数据时能够直接提供。
最后我们要思考的就是:利用二分法后,如何判断mid值是否合法。经过思考,小编是利用动态规划来进行判断的。根据题目需求,可以很容易就想到动态规划的数组可以设计为dp[i][j][ii],其中ii是一个k位数,对应的位数代表着该食材的运输情况:1,代表该食材已经被运输;0,代表该食材没有被运输,而dp[i][j][ii]代表第i个节点,已经设立j个检查站,食材运输情况为ii时是否满足条件。如果这个情况满足条件,则dp[i][j][ii]=1,否则dp[i][j][ii] = 0;可是经过更加仔细的分析,可以发现dp[i][j][ii]种的i是可以省略的,也就是如上的动态规划数组是可以进行状态压缩的,可以将其状态压缩为dp[j][ii],而我们只需要在这之前对每一个节点进行循环即可,这个状态压缩可以减少空间的开销。而判断mid值是否合法,就是判断dp[m][(1<<k)-1]是否为1,如果为1,二分则可以继续缩小;否则,要扩大。具体代码如下。
代码详解
1、全局变量声明
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
int n, m, k;
int request[101]; //利用位运算,记录每一个酒店是否需要该石材
int flag[101]; //记录特定酒店对食材需要的情况
int road;
int G[101][20]; //存放车从每一个酒店节点出发送每一种食材的所需时间的最大值
int dp[12][1025], tmp[12][1025];
int T[101];
struct node
{
int v, w;
};
vector<node>graph[101];
2、建图
利用输入以及vector数组进行建图
int i, j;
//输入
cin >> n >> m >> k;
//记录每一个酒店对食材的需要情况
for (i = 1; i <= n; i++)
{
for (j = 0; j < k; j++)
{
int x;
cin >> x;
if (x == 1) request[i] |= (1 << j); //位运算的思想
}
}
//双向建图
for (i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({ v,w });
graph[v].push_back({ u,w });
}
3、预处理
算法分析种的预处理过程,计算存储每一个酒店节点作为每一种食材的检查站时的所有酒店的等待时间的最小的最大值,以便后面的二分已经动态规划中使用。利用深度优先搜索dfs来进行,具体代码如下:
//预处理
for (i = 1; i <= n; i++)
{
for (j = 0; j < k; j++)
{
memset(flag, 0, sizeof(flag));
for (int ii = 0; ii <= n; ii++)
if ((request[ii] >> j) & 1) flag[ii] = 1;
road = 0;
//从该出发点到最远的叶子节点酒店的最远距离
int mx = dfs(i, -1);
//算法分析中已经推导了这个公式
G[i][j] = road * 2 - mx;
}
}
而dfs过程中进行深度优先搜索的时候需要额外考虑该节点的酒店对该食材是否有需求,只要在搜索的过程中加一个判断即可,具体代码如下:
int dfs(int u, int fa)
{
int mx = 0;
int i;
for (i = 0; i < graph[u].size(); i++)
{
int v = graph[u][i].v;
int w = graph[u][i].w;
if (v == fa) continue;//防止重复计算
int re = dfs(v, u);//深度优先搜索
if (flag[v])//这个节点酒店对该食材有需求
{
flag[u] = 1;
road = road + w;
mx = max(mx, w + re);
}
}
return mx;
}
4、二分法
很普通的二分,代码如下:
int l = 1, r = 1e9, ans;
while (l <= r)
{
int mid = (l + r) >> 1;
if (judge(mid))//满足条件就继续缩小
{
r = mid - 1;
ans = mid;
}
else l = mid + 1; //不满足条件,就进行扩大
}
cout << ans;
5、动态规划
如上二分法,juege()过程是利用动态规划的,代码如下:
int judge(int mid)
{
int i, j, ii;
memset(dp, 0, sizeof(dp)); //动态规划数组
memset(T, 0, sizeof(T)); //记录每一个节点在满足mid的条件下可以运输食材的情况
for (i = 1; i <= n; i++)
for (j = 0; j < k; j++)
if (G[i][j] <= mid) T[i] |= (1 << j);
tmp[0][0] = dp[0][0] = 1; //初始化
for (i = 1; i <= n; i++) //枚举每一个节点
{
for (j = 1; j <= m; j++)
{
memcpy(tmp, dp, sizeof(dp)); //状态压缩带来的状态复制
for (ii = 0; ii < (1 << k); ii++) //枚举,每一种已经运输的情况
{
dp[j][ii] |= tmp[j][ii];
dp[j][ii | T[i]] |= tmp[j - 1][ii]; //状态更新
}
}
}
return dp[m][(1 << k) - 1]; //返回是否满足条件
}
完整代码
满分代码如下:
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
int n, m, k;
int request[101]; //利用位运算,记录每一个酒店是否需要该石材
int flag[101]; //记录特定酒店对食材需要的情况
int road;
int G[101][20]; //存放车从每一个酒店节点出发送每一种食材的所需时间的最大值
int dp[12][1025], tmp[12][1025];
int T[101];
struct node
{
int v, w;
};
vector<node>graph[101];
int dfs(int u, int fa)
{
int mx = 0;
int i;
for (i = 0; i < graph[u].size(); i++)
{
int v = graph[u][i].v;
int w = graph[u][i].w;
if (v == fa) continue;//防止重复计算
int re = dfs(v, u);//深度优先搜索
if (flag[v])//这个节点酒店对该食材有需求
{
flag[u] = 1;
road = road + w;
mx = max(mx, w + re);
}
}
return mx;
}
int judge(int mid)
{
int i, j, ii;
memset(dp, 0, sizeof(dp)); //动态规划数组
memset(T, 0, sizeof(T)); //记录每一个节点在满足mid的条件下可以运输食材的情况
for (i = 1; i <= n; i++)
for (j = 0; j < k; j++)
if (G[i][j] <= mid) T[i] |= (1 << j);
tmp[0][0] = dp[0][0] = 1; //初始化
for (i = 1; i <= n; i++) //枚举每一个节点
{
for (j = 1; j <= m; j++)
{
memcpy(tmp, dp, sizeof(dp)); //状态压缩带来的状态复制
for (ii = 0; ii < (1 << k); ii++) //枚举,每一种已经运输的情况
{
dp[j][ii] |= tmp[j][ii];
dp[j][ii | T[i]] |= tmp[j - 1][ii]; //状态更新
}
}
}
return dp[m][(1 << k) - 1]; //返回是否满足条件
}
int main()
{
int i, j;
//输入
cin >> n >> m >> k;
//记录每一个酒店对食材的需要情况
for (i = 1; i <= n; i++)
{
for (j = 0; j < k; j++)
{
int x;
cin >> x;
if (x == 1) request[i] |= (1 << j); //位运算的思想
}
}
//双向建图
for (i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({ v,w });
graph[v].push_back({ u,w });
}
//预处理
for (i = 1; i <= n; i++)
{
for (j = 0; j < k; j++)
{
memset(flag, 0, sizeof(flag));
for (int ii = 0; ii <= n; ii++)
if ((request[ii] >> j) & 1) flag[ii] = 1;
road = 0;
//从该出发点到最远的叶子节点酒店的最远距离
int mx = dfs(i, -1);
//算法分析中已经推导了这个公式
G[i][j] = road * 2 - mx;
}
}
int l = 1, r = 1e9, ans;
while (l <= r)
{
int mid = (l + r) >> 1;
if (judge(mid))//满足条件就继续缩小
{
r = mid - 1;
ans = mid;
}
else l = mid + 1; //不满足条件,就进行扩大
}
cout << ans;
return 0;
}