AtCoder - ABC 267 - E(二分/贪心)

E - Erasing Vertices 2

题意:

给定一个有 n 个点 m 条边无向图,每个点都有权值。每次要删除一个点及其所连的所有边,删除某点的代价是与这个点直接相邻的所有点的权值和,使 n 次删除操作后的最大值最小,求其最小值。

数据范围:

1 ≤ N ≤ 2 × 10^{5}

0 ≤ M ≤ 2 × 10^{5}

1 ≤ Ai​ ≤ 10^{9}

1 ≤ Ui​,Vi ​≤ N 

思路1:(二分)

最大值的最小值,典型的二分问题。

通过搜索将代价小于等于当前二分的值的点删除,如果所有点最终都能被删除,说明当前二分的代价可行。

实现:1.这里用的是邻接表存图;数组w记录删除某点的代价;数组st表示节点是否被删除过。

2.用队列实现先将代价小于等于当前二分的值x的点删掉,将这样的点入队后。从头遍历队列,执行删除队头节点后将与该点直接相连的所有节点的代价值更新,再判断更新后的点的代价是否小于等于 x,如果是,入队表示该点可以作为在代价值为 x 时删除的节点。

3.最后判断是否存在节点未被删除,如果存在,说明当前的 x 太小,继续进行二分枚举。

Code:

#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;
}

思路2:(贪心)

如何贪心最优:
因为删除节点的顺序对最后图的结构是没有影响的,即最终 n 个节点都会被删除。所以我们每次选点操作时应该尽量先删去代价小的节点,这样不会使得之后的操作更劣,这就是最优的贪心做法。

如何动态更新操作后选择代价最小的节点呢:
朴素做法会超时 O(n^2),我们可以考虑用小根堆,即优先队列来优化。将 n 个节点的代价入队,每取队头进行删除操作后,将与该节点直接相连的点的代价更新。将更新后的值入队。这样在删除点的过程中,所需的的最大代价就是要求的最大值的最小值。

实现:1.这里是用二维数组存的图;优先队列存的是节点的代价及节点编号;数组w记录删除某点的代价;数组st表示节点是否被删除过。

2.将所有节点都入队后,每次取堆顶节点进行删除操作,记录每次删除的代价,最后取Max即最终答案;同理将与被删除节点直接相连的其他节点的代价值更新后,再次入队。

时间复杂度:O(logn)

Code:

#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。

你可能感兴趣的:(AtCoder,算法)