1h40min.
1500pts(ABCDF), vp.
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;
}
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;
}
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;
}
想到质因数分解,然后枚举阶乘。但对于这题的数据量,肯定是不可能的。
这题从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;
}
两道数学题摆在那么前面,概率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(i−2)×(P/100)+dp(i−1)×(1−P/100)).应该是对的,但手算出来是错的。
先不论正确性,这题要输出的,不是浮点数,而是要用逆元,这令我完全不知道怎么转移了。
两点不连通,输出nan。
两点连通,有正环,就输出inf;没有正环,就输出最长距离。
想到直接spfa,肯定TLE。
在分析之前,根据这题的数据范围,我便猜到了要建成一棵树。
进一步分析,这题的特殊性在于,一条边,两种方向走,权值互为相反数。我们不是要判正环吗?事实上,正环与负环相伴相生。所以,输出距离的情况,就是这个连通块内所有的环权值都是0.
这意味着,两点间距离唯一。连那么多边是毫无用处的。
所以,我们保留其中 m − 1 m-1 m−1条边,把它建成一棵树,对其中的两点跑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;
}
上文已说。
代码麻烦的地方在于,需要在输入时先记录下来多余的边,等建完树后,再判断连通块内是否有正环。
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;
}
让我们梳理一下目前为止的思路。
每一个连通块,被我们简化成一棵树,整个图变成一个森林。在每一个树上,我们用多余的边判正环。如果没有正环,算树上两点距离。我们将多源最短路,变成单源的唯一路径。
并查集,森林,树上距离。比赛时我就感觉,这题与上海市计算机学会竞赛平台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;
}
想到上面算法的错因: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;
}
上文所说的,最后剩下一个质数,在分解质因数中是这样的。
对于本题,很有可能 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(klogk)。
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;
}
绕了半天,变成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题,没看出什么。
关于这题对数据的处理,是我的知识盲区了。和正常用浮点数一样,把所有除法改成乘逆元即可。
但是关于初始化、状态转移,我还有很多不懂的地方。
等我好好学了概率dp之后,再来看这种题吧。
这次补题有点痛苦,E还补不动了。
每次打ABC都做不出G,希望能补掉几个。