AtCoder ABC280 A~F

1h40min.
1500pts(ABCDF), vp.

A - Pawn on a Grid

不讲 - AC

int main() {
	int n, m, ans = 0;
	setIO("");
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			char ch; cin >> ch;
			if (ch == '#') ans++;
		}
	}
	cout << ans;
	
	return 0;
}

B - Inverse Prefix Sum

差分 - AC

int n;
ll s[MAXN];
 
int main() {
	setIO("");
	
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> s[i];
		cout << s[i] - s[i - 1] << ' ';
	} cout << "\n";
	
	
	return 0;
}

C - Extra Character

判不等 - AC

string s, t;
 
int main() {
	setIO("");
	
	cin >> s >> t;
	for (int i = 0; i < s.size(); ++i) {
		if (s[i] != t[i]) {
			cout << i + 1 << "\n";
			return 0;
		}
	}
	cout << t.size() << "\n";
	
	
	return 0;
}

D - Factorial and Multiple

分析 - WA

想到质因数分解,然后枚举阶乘。但对于这题的数据量,肯定是不可能的。

这题从1到n,每个数只能用一遍。所以想到,从2开始枚举n,如果能整除k,那就除掉,相当于把这个数用了一遍,直到k=1了。
然而,当k是一个大质数,又要TLE了。所以我根据质因数分解的经验判断,先枚举 [ 2 , k ] [2, \sqrt{k}] [2,k ],全部除过之后,应该会剩下一个大于 k \sqrt{k} k 的质数,如果有;那答案就是它。

交上去WA了,想了几分钟,找不到错误。于是决定先看后面的题。

事实上,这个算法的漏洞太多,下文将分析。

ll k, ans;
 
int main() {
	setIO("");
	
	cin >> k;
	ll t = k;
	for (ll i = 2; i * i <= k; ++i){
		if (!(t % i)) {
			ckmax(ans, i);
			t /= i;
		}
	} ckmax(ans, t);
	
	cout << ans << "\n";
	
	return 0;
}

E - Critical Hit

概率dp想法

两道数学题摆在那么前面,概率dp我也没做几道,这次做得有点艰难。

定义 d p ( i ) dp(i) dp(i),造成i点伤害的期望攻击次数。
想了一个转移方程 d p ( i ) = 1 + ( d p ( i − 2 ) × ( P / 100 ) + d p ( i − 1 ) × ( 1 − P / 100 ) ) dp(i)=1+(dp(i-2)\times(P/100)+dp(i-1)\times(1-P/100)) dp(i)=1+(dp(i2)×(P/100)+dp(i1)×(1P/100)).应该是对的,但手算出来是错的。
先不论正确性,这题要输出的,不是浮点数,而是要用逆元,这令我完全不知道怎么转移了。

F - Pay or Receive

SPFA、并查集、树 - TLE

两点不连通,输出nan。
两点连通,有正环,就输出inf;没有正环,就输出最长距离。
想到直接spfa,肯定TLE。

在分析之前,根据这题的数据范围,我便猜到了要建成一棵树。

进一步分析,这题的特殊性在于,一条边,两种方向走,权值互为相反数。我们不是要判正环吗?事实上,正环与负环相伴相生。所以,输出距离的情况,就是这个连通块内所有的环权值都是0.
这意味着,两点间距离唯一。连那么多边是毫无用处的。
所以,我们保留其中 m − 1 m-1 m1条边,把它建成一棵树,对其中的两点跑LCA求距离。

关于本题的树上距离,一开始没仔细想,把lca打完了才发现,这题不同于别的题,它是有向边。儿子到父亲,和父亲到儿子,边权是相反数。因此,简单地,x到根,根到y的有向距离加起来即可 d i s ( x ) + d i s ( y ) dis(x)+dis(y) dis(x)+dis(y).

关于判正环,我在想出这个思路后又想到,当一条新的边连接了两个已在同一连通块内的点,判断它满不满足“两点间距离唯一”的性质即可。
不过当时想,这样写起来好像有点麻烦,所以还是写了spfa。事实上,spfa本身算法复杂度就不稳定,再加上对每个连通块都要跑一边,频繁地memset,很容易被数据卡掉,写起来还麻烦
要树立起仔细算算法复杂度的意识

const int MAXN = 2e5 + 5, MAXP = 20;

struct Edge {
	int v; ll w;
	Edge(int _v, ll _w): v(_v), w(_w){}
};

int n, m, q, fa[MAXN], cnt[MAXN];
ll dis[MAXN], dsi[MAXN];
bool vis[MAXN], vsi[MAXN], inf[MAXN];
vector<Edge> g[MAXN], og[MAXN];

int find(int x) {
	return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
	fa[find(x)] = find(y);
}

void predfs(int u, int par) {
	for (Edge e : g[u]) {
		int v = e.v; ll w = e.w;
		if (v == par) continue;
		dsi[v] = dsi[u] + w;
		predfs(v, u);
	}
}

int main() {
	setIO("");
	
	cin >> n >> m >> q;
	for (int i = 1; i <= n; ++i) fa[i] = i;
	for (int i = 1; i <= m; ++i) {
		int a, b; ll c; cin >> a >> b >> c;
		og[a].push_back(Edge(b, c));
		og[b].push_back(Edge(a, -c));
		if (find(a) != find(b)) {
			g[a].push_back(Edge(b, c));
			g[b].push_back(Edge(a, -c));
			merge(a, b);
		}
	}
	
	for (int i = 1; i <= n; ++i) {
		if (!vsi[find(i)]) {
			if (!spfa(i)) {
				inf[find(i)] = true;
			}
			else {
				predfs(i, 0);
			}
			vsi[find(i)] = true;
		}
	}
	
	while (q--) {
		int x, y; cin >> x >> y;
		if (find(x) != find(y)) {
			cout << "nan\n";
		}
		else if (inf[find(x)]) {
			cout << "inf\n";
		}
		else {
			cout << -dsi[x] + dsi[y] << "\n";
		}
	}
	
	return 0;
}

用性质判正环 - AC

上文已说。

代码麻烦的地方在于,需要在输入时先记录下来多余的边,等建完树后,再判断连通块内是否有正环。

struct Edge {
	int v; ll w;
	Edge(int _v, ll _w): v(_v), w(_w){}
};
 
int n, m, q, fa[MAXN];
ll dsi[MAXN];
bool vsi[MAXN], inf[MAXN];
vector<int> fro;
vector<Edge> g[MAXN], es;
 
int find(int x) {
	return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
	fa[find(x)] = find(y);
}
 
void predfs(int u, int par) {
	for (Edge e : g[u]) {
		int v = e.v; ll w = e.w;
		if (v == par) continue;
		dsi[v] = dsi[u] + w;
		predfs(v, u);
	}
}
 
int main() {
	setIO("");
	
	cin >> n >> m >> q;
	for (int i = 1; i <= n; ++i) fa[i] = i;
	for (int i = 1; i <= m; ++i) {
		int a, b; ll c; cin >> a >> b >> c;
		if (find(a) != find(b)) {
			g[a].push_back(Edge(b, c));
			g[b].push_back(Edge(a, -c));
			merge(a, b);
		}
		else {
			fro.push_back(a);
			es.push_back(Edge(b, c));
		}
	}
	
	for (int i = 1; i <= n; ++i) {
		if (!vsi[find(i)]) {
			predfs(i, 0);
			vsi[find(i)] = true;
		}
	}
	for (int i = 0; i < es.size(); ++i) {
		if (!inf[find(fro[i])] && -dsi[fro[i]] + dsi[es[i].v] != es[i].w) {
			inf[find(fro[i])] = true;
		}
	}
	
	while (q--) {
		int x, y; cin >> x >> y;
		if (find(x) != find(y)) {
			cout << "nan\n";
		}
		else if (inf[find(x)]) {
			cout << "inf\n";
		}
		else {
			cout << -dsi[x] + dsi[y] << "\n";
		}
	}
	
	return 0;
}

SPFA、森林转树 - AC(补)

让我们梳理一下目前为止的思路。
每一个连通块,被我们简化成一棵树,整个图变成一个森林。在每一个树上,我们用多余的边判正环。如果没有正环,算树上两点距离。我们将多源最短路,变成单源的唯一路径。

并查集,森林,树上距离。比赛时我就感觉,这题与上海市计算机学会竞赛平台2022年11月月赛乙组 总结中的T4是极其相似的。但当时我没有想过,可以仿照那一题,以0为虚根,把森林变成树,简化代码。
发现题目与之前做过的题有相似性时,不妨大胆动用经验

上文中,我们先用了spfa。然后,我们分析问题性质,用一个“不速之客”,一个僻不当道的算法代替了它,但变得比较麻烦了。
为什么不运用前人的智慧,改造spfa?
本题两点间距离唯一,所以原本spfa中一个点最多入队n次,可改为1次。
事实上,也能看出上面遍历多余的边的想法与spfa的统一性。spfa是在搜索中做的,更方便。
经典算法是可以根据问题性质被改造的

清晰且简单了不是一点半点。

struct Edge {
	int v; ll w;
	Edge(int _v, ll _w): v(_v), w(_w){}
};
 
int n, m, q, fa[MAXN];
bool inf[MAXN];
ll dis[MAXN];
vector<Edge> g[MAXN];
 
int find(int x) {
	return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
	fa[find(x)] = find(y);
}
 
void dfs(int u) {
	if (inf[find(u)]) return;
	for (int i = 0; i < g[u].size(); ++i) {
		int v = g[u][i].v; ll w = g[u][i].w;
		if (dis[v] != LNF) {
			if (dis[u] + w != dis[v]) inf[find(u)] = true;
		}
		else {
			dis[v] = dis[u] + w;
			dfs(v);
		}
	}
}
 
int main() {
	setIO("");
	
	cin >> n >> m >> q;
	for (int i = 1; i <= n; ++i) fa[i] = i;
	for (int i = 1; i <= m; ++i) {
		int a, b; ll c; cin >> a >> b >> c;
		merge(a, b);
		g[a].push_back(Edge(b, c));
		g[b].push_back(Edge(a, -c));
	}
	
	for (int i = 1; i <= n; ++i) {
		if (fa[i] == i) g[0].push_back(Edge(i, 0));
	}
	memset(dis, 0x3f, sizeof(dis)); dis[0] = 0;
	dfs(0);
	
	while (q--) {
		int x, y; cin >> x >> y;
		if (find(x) != find(y)) cout << "nan\n";
		else if (inf[find(x)]) cout << "inf\n";
		else cout << -dis[x] + dis[y] << "\n";
	}
	
	return 0;
}

D - Factorial and Multiple

gcd - WA

想到上面算法的错因:k不一定是在阶乘中的整数中选出几个乘起来而得到的。可能是,比如,14贡献了一个因子7,而22贡献了一个因子11.
所以,刚刚枚举的那些整数,只要不与剩下的k互质,都能做出贡献。

WA了3个点。看来是“枚举 [ 2 , k ] [2, \sqrt{k}] [2,k ],全部除过之后,应该会剩下一个大于 k \sqrt{k} k 的质数”的问题了。

ll k, ans;
 
ll gcd(ll x, ll y) {
	return !(x % y) ? y : gcd(y, x % y);
}
 
int main() {
	setIO("");
	
	cin >> k;
	ll t = k;
	for (ll i = 2; i * i <= k; ++i){
		if (gcd(i, t) != 1) {
			t /= gcd(i, t);
			ckmax(ans, i);
		}
	} ckmax(ans, t);
	
	cout << ans << "\n";
	
	return 0;
}

枚举 - AC

上文所说的,最后剩下一个质数,在分解质因数中是这样的。
对于本题,很有可能 k \sqrt{k} k 之前的因子还不足以把k消成一个质数。这就是为什么上面的算法WA 3个点。

为了保证正确性,我们大抵需要 O ( k ) O(k) O(k)枚举,那样会超时。
就算忽略只WA 3个点也能想到,因子缺得应该不多。不妨把枚举范围乘一个小常数试试。
一不小心过了,哈哈。

时间复杂度 O ( k l o g k ) O(\sqrt{k}log\sqrt{k}) O(k logk )

ll k, ans;
 
ll gcd(ll x, ll y) {
	return !(x % y) ? y : gcd(y, x % y);
}
 
int main() {
	setIO("");
	
	cin >> k;
	ll t = k;
	for (ll i = 2; i * i / 10 <= k; ++i){
		if (gcd(i, t) != 1) {
			t /= gcd(i, t);
			ckmax(ans, i);
		}
	} ckmax(ans, t);
	
	cout << ans << "\n";
	
	return 0;
}

分解质因数 - AC(补)

绕了半天,变成gcd。
从AtCoder ABC276 A~F的D题中也能看出,gcd与质因数分解的联系
所以还是回到了最开始的分解质因数啊。单对分解质因数来说,时间复杂度是 O ( k ) O(\sqrt{k}) O(k )的,其实不超。
分解之后,可以像一开始说的,枚举n,计算它们对质因子的贡献。肯定TLE。
不如要什么找什么。比如k有10个质因子2,就枚举2、4、6、8……直到2的个数够了。这样k不断除以质因子,时间复杂度 O ( l o g k ) O(logk) O(logk).
要厘清的是,总的时间复杂度是 O ( k + l o g k ) O(\sqrt{k}+logk) O(k +logk),不讲。

这题挺简单的,想偏了。

ll k, ans;
 
int main() {
	setIO("");
	
	cin >> k;
	for (ll i = 2; i * i <= k; ++i) {
		int p = 0, q = 0; ll n = 0;
		while (!(k % i)) k /= i, ++p;
		while (q < p) { 
			n += i;
			ll tmp = n;
			while (!(tmp % i)) ++q, tmp /= i;
		}
		ckmax(ans, n);
	} ckmax(ans, k);
	cout << ans << "\n";
	
	return 0;
}

E - Critical Hit

最后十分钟,我回来看E题,没看出什么。

概率dp想法(嗷嗷待补)

关于这题对数据的处理,是我的知识盲区了。和正常用浮点数一样,把所有除法改成乘逆元即可

但是关于初始化、状态转移,我还有很多不懂的地方。
等我好好学了概率dp之后,再来看这种题吧。


这次补题有点痛苦,E还补不动了。
每次打ABC都做不出G,希望能补掉几个。

你可能感兴趣的:(#,打比赛,算法,图论)