Luogu CF76A

CF76A Gift

题意

我认为最好的题意Portal感谢 @HTensor 的更好的翻译,否则我看不懂正确的题意。

思路

壹:初始的暴力思路

按照每条边的 Ag 的需求数量从小到大排序,然后枚举最大 Au 数目的大小 mg(max gold),用 Au 数目小于等于 mg 的边进行 Kruskal 然后求最小值。

时间复杂度 \(O(\max(Au) \times M)\), 这里的 Au 代指 Au 的数目。

贰:稍微优化了一下

再开一个数组存 Au 的数目,从小到大排序后枚举那个数组的元素大小

时间复杂度 \(O(M^2)\)

叁:最重要的优化

先来思考这么一个问题。

再 mg 小的时候,满足 Au 的数目的边尚且没被选入生成树中,那么当循环到后面时 mg 变得很大这条边是否会被选入到生成树中去?

显然不会,范围小时尚且不行,范围扩大之后哪里还会有这条边的份呢?

所以,我们显然可以把那些没用的边在循环之后删了从而得到更好的时间复杂度

我选用的是用 STL 中的 multiset 不去重的集合,好处是我可以遍历每条边,还可以以 log 级别的复杂度删除边。

时间复杂度 \(O(M + M \times N \times log_2N)\),可以过。

肆:一些可以但没必要的优化

  • 优化一 : Kruskal 中判断是否可以提前结束循环的优化。
  • 优化二 : 在核心循环的前面用二分定位第一个可以形成生成树的最小的 Au 的数目,之后可以从它开始跑。
  • 优化三 : 可以在排序后把 multiset 改成支持删除的双向链表,可以把上面那个复杂度中的 log 去掉(这个我没写)。

你们最想要的代码

# include <bits/stdc++.h>
using namespace std;

const int maxn = 205;
const int maxm = 50005;

struct reader {
	template <typename Type>
	reader & operator >> (Type & ret) {
		int f = 1; ret = 0; char ch = getchar ();
		for (;!isdigit (ch); ch = getchar ()) if (ch == '-') f=-f;
		for (; isdigit (ch); ch = getchar ()) ret = (ret << 1) + (ret << 3) + (ch - '0');
		ret *= f; return *this;
	}
} fin; // 快读,可以像cin一样读整型数据,其他不行

int N, M, G, S;
struct Edge {
	int x, y, g, s;
	bool operator < (Edge X) const {
		return s < X.s;
	}
} P; // 边的结构体
multiset < Edge > E; // 上文提到的集合
int gg[maxm], L, R, mid, res; // 这里的gg数组是上文说的把 Au 单独出来排序
int fa[maxn];
long long ans, now;

int getfa (int id) {return id == fa[id] ? id : fa[id] = getfa (fa[id]);}

bool check (int maxg) {
	int cnt = 1;
	for (int i = 1; i <= N; i++) fa[i] = i;
	for (multiset < Edge > :: iterator it = E.begin (); it != E.end (); it++) {
		P = *it; int fx = getfa (P.x), fy = getfa (P.y);
		if (fx == fy || P.g > maxg) continue; fa[fx] = fy; cnt++;
	}
	// 二分check,multiset看不懂的去上文中的链接
	return cnt == N;
}

int main () {
	fin >> N >> M >> G >> S;
	for (int i = 1; i <= M; i++)
	fin >> P.x >> P.y >> P.g >> P.s, gg[i] = P.g, E.insert (P);
	sort (gg + 1, gg + 1 + M); L = 1; R = M; res = -1;
	while (L <= R) { // 优化二,可以去掉,不过下面的特判要注意
		mid = L + R >> 1;
		check (gg[mid]) ? R = mid - 1, res = mid : L = mid + 1;
	}	ans = 9223372036854775800; // long long 类型上限9223372036854775807
	if (res == -1) {printf ("-1"); return 0;}
	// 如果无论如何都不可以形成生成树,那么输出 -1
	for (int mg = res; mg <= M; mg++) {
		// 刷Kruskal
		int cnt = 1; now = 0;
		for (int i = 1; i <= N; i++) fa[i] = i;
		for (multiset < Edge > :: iterator it = E.begin (); it != E.end (); it++) {
			P = *it; int fx = getfa (P.x), fy = getfa (P.y);
			if (P.g > gg[mg]) continue; if (fx == fy) {
				// 如果满足上文说的优化的条件那就把这个点去掉
				multiset < Edge > :: iterator registit = it;
				registit--; E.erase (it); it = registit; continue;
			}	fa[fx] = fy; now = P.s; cnt++;
			if (cnt == N) break; // 优化一,记录当前有多少个节点在生成树中
		}
		ans = min (ans, now * S + (long long) gg[mg] * G);
	}
	cout << ans << endl;
	return 0;
}

关于其他的一些解释

一:关于删边时的集合迭代器更换

我是先把当前迭代器的前驱记录下来。

multiset < Edge > :: iterator registit = it;
registit--;

在集合删除迭代器 it 之后,it 应该已经不在集合 E 中了,所以不能直接++,而是类似于链表一样让它变成它的前驱,之后再在循环中自动++。

E.erase (it);
it = registit; continue;

然后 it 就会正常的向下一位传递。

二:如果链接失效等

如果链接失效等或者有什么没理解的私信我,我会进行补充说明

Made By \(\text{wheneveright}\)

posted @ 2021-04-12 17:20  XJ21  阅读(60)  评论(0)    收藏  举报