给定一个有 n 个点 m 条边无向图,每个点都有权值。每次要删除一个点及其所连的所有边,删除某点的代价是与这个点直接相邻的所有点的权值和,使 n 次删除操作后的最大值最小,求其最小值。
1 ≤ N ≤ 2 ×
0 ≤ M ≤ 2 ×
1 ≤ Ai ≤
1 ≤ Ui,Vi ≤ N
求最大值的最小值,典型的二分问题。
通过搜索将代价小于等于当前二分的值的点删除,如果所有点最终都能被删除,说明当前二分的代价可行。
实现:1.这里用的是邻接表存图;数组w记录删除某点的代价;数组st表示节点是否被删除过。
2.用队列实现先将代价小于等于当前二分的值x的点删掉,将这样的点入队后。从头遍历队列,执行删除队头节点后将与该点直接相连的所有节点的代价值更新,再判断更新后的点的代价是否小于等于 x,如果是,入队表示该点可以作为在代价值为 x 时删除的节点。
3.最后判断是否存在节点未被删除,如果存在,说明当前的 x 太小,继续进行二分枚举。
#include
#include
#include
#include
#include
using namespace std;
const int N = 200010;
#define int long long
int n, m;
int a[N];
int h[N], e[2 * N], ne[2 * N], idx;
//邻接表存图
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
bool check(int x)
{
vectorw(n + 1); //数组w[j]记录节点j被删除的代价,即点j与其直接相连的所有点的权值之和
for (int i = 1; i <= n; i++) //i表示枚举的起点
for (int j = h[i]; ~j; j = ne[j]) //枚举的j对应的是直接与起点i相连的点
w[e[j]] += a[i]; //因为j与i相连,所以预处理j对应的节点代价时,算上i节点的权值
queueq;
for (int i = 1; i <= n; i++)
if (w[i] <= x)
q.push(i); //将价值比此时枚举到的答案x小于等于的节点,考虑为所要删除的点
vectorst(n + 1); //标记数组,用来记录节点是否被删除过
while (q.size())
{
int u = q.front(); //取队头来遍历队列
q.pop();
if (st[u])continue; //如果该节点已经删除过,跳过
st[u] = true; //标记下
for (int i = h[u]; ~i; i = ne[i]) //要删除点u,遍历所有与节点u相连的节点
{
int j = e[i]; //j即表示与节点u直接相连的点
if (st[j])continue;
w[j] -= a[u]; //删除节点u后,节点j的代价减去节点u的权值
if (w[j] <= x)q.push(j); //判断此时的节点j可以作为删除点,如果是入队
}
}
for (int i = 1; i <= n; i++)if (!st[i])return false; //如果最后有节点没有标记过,说明要删除该节点所需的代价大于x,继续二分增更大的x的
return true;
}
void solve()
{
cin >> n >> m;
memset(h, -1, sizeof(h));
for (int i = 1; i <= n; i++)cin >> a[i]; //输入n个点的权值
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a); //创建边
}
int l = -1, r = 1e18;
while (l < r)
{
int mid = l + r >> 1;
if (check(mid))r = mid;
else l = mid + 1;
}
cout << l << endl;
}
signed main()
{
int t = 1;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
如何贪心最优:
因为删除节点的顺序对最后图的结构是没有影响的,即最终 n 个节点都会被删除。所以我们每次选点操作时应该尽量先删去代价小的节点,这样不会使得之后的操作更劣,这就是最优的贪心做法。
如何动态更新操作后选择代价最小的节点呢:
朴素做法会超时 O(n^2),我们可以考虑用小根堆,即优先队列来优化。将 n 个节点的代价入队,每取队头进行删除操作后,将与该节点直接相连的点的代价更新。将更新后的值入队。这样在删除点的过程中,所需的的最大代价就是要求的最大值的最小值。
实现:1.这里是用二维数组存的图;优先队列存的是节点的代价及节点编号;数组w记录删除某点的代价;数组st表示节点是否被删除过。
2.将所有节点都入队后,每次取堆顶节点进行删除操作,记录每次删除的代价,最后取Max即最终答案;同理将与被删除节点直接相连的其他节点的代价值更新后,再次入队。
时间复杂度:O(logn)
#include
#include
#include
#include
#include
using namespace std;
#define x first
#define y second
#define int long long
const int N = 200010;
typedef pairPII;
int n, m;
int a[N], w[N];
bool st[N];
vectorG[N];
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> a[i];
while (m--)
{
int a, b;
cin >> a >> b;
//用二维数组G存储树
G[a].push_back(b);
G[b].push_back(a);
}
for (int i = 1; i <= n; i++)
for (auto v : G[i])
w[i] += a[v]; //用数组w记录删除节点的代价
priority_queue, greater>pq; //优先队列(小根堆),每个元素类型是PII
for (int i = 1; i <= n; i++)pq.push(make_pair(w[i], i)); //将每个节点及代价入队,代价在前,优先代价排序
int ans = 0;
while (!pq.empty())
{
PII now = pq.top(); //按代价最小的点先删除;即每次先取堆顶元素
pq.pop();
if (st[now.y])continue;
st[now.y] = true; //st数组记录节点是否被删除
ans = max(ans, now.x); //贪心下求最大价值即最终答案最小的最大值
for (auto v : G[now.y]) //枚举与节点now直接相连的节点v
{
w[v] -= a[now.y]; //将节点v更新为删除节点now后的代价
pq.push(make_pair(w[v], v)); //将节点v入队
}
}
cout << ans << endl;
}
signed main()
{
int t = 1;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:想到用二分,但是check的思路无啊。题目注意点:
1.二分和贪心代码上其实有相似性的,尤其是删除点的相关操作,类似于 Dijkstra 算法,边操作边更新。
2.贪心代码中的make_pair函数倒是第一次见,感觉挺好用。还有注意pair排序优先first。