【2018ICPC南京】2018-2019 ACM-ICPC, Asia Nanjing Regional Contest部分题解(D,I,J,K,M)
2018-2019 ACM-ICPC, Asia Nanjing Regional Contest部分题解
题目链接:Dashboard - 2018-2019 ACM-ICPC, Asia Nanjing Regional Contest - Codeforces
D. Country Meow(模拟退火)
一开始用三分写,答案没跑出来,后来才知道是模拟退火(纪录一下第一次写模拟退火)。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr double eps = 1e-3;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout << fixed << setprecision(8);
int n;
cin >> n;
vector<double> x(n), y(n), z(n);
for (int i = 0; i < n; i++) {
cin >> x[i] >> y[i] >> z[i];
}
auto cal = [&](double x1, double y1, double z1, double x2, double y2, double z2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2));
};
double S_T = 1000, F_T = eps;
double rate = 0.98;
double T = S_T;
double ax = 0, ay = 0, az = 0;
double mi = 1e50;
while (T > F_T) {
double mx = x[0], my = y[0], mz = z[0];
for (int i = 1; i < n; i++) {
if (cal(ax, ay, az, x[i], y[i], z[i]) > cal(ax, ay, az, mx, my, mz)) {
mx = x[i], my = y[i], mz = z[i];
}
}
mi = min(mi, cal(ax, ay, az, mx, my, mz));
ax += (mx - ax) * (T / S_T);
ay += (my - ay) * (T / S_T);
az += (mz - az) * (T / S_T);
T *= rate;
}
cout << mi << '\n';
return 0;
}
I. Magic Potion(最大流)
一眼最大流,但比赛时死活想不出怎么分配那k瓶药水,甚至写了先跑n流量,再跑k流量,分两次来跑的最大流,然后当然是wa了。
赛后发现直接建两个源点分别连接n个法师,其中一个分n流量,一个分k流量,然后直接跑最大流就好了。。。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct Flow {
static constexpr int INF = 1e9;
int n;
struct Edge {
int to, cap;
Edge(int to, int cap) : to(to), cap(cap) {}
};
vector<Edge> e;
vector<vector<int>> g;
vector<int> cur, h;
Flow(int n) : n(n), g(n) {}
bool bfs(int s, int t) {
h.assign(n, -1);
queue<int> que;
h[s] = 0;
que.push(s);
while (!que.empty()) {
int u = que.front();
que.pop();
for (int i : g[u]) {
auto [v, c] = e[i];
if (c > 0 && h[v] == -1) {
h[v] = h[u] + 1;
if (v == t)
return true;
que.push(v);
}
}
}
return false;
}
int dfs(int u, int t, int f) {
if (u == t)
return f;
int r = f;
for (int &i = cur[u]; i < int(g[u].size()); ++i) {
int j = g[u][i];
auto [v, c] = e[j];
if (c > 0 && h[v] == h[u] + 1) {
int a = dfs(v, t, min(r, c));
e[j].cap -= a;
e[j ^ 1].cap += a;
r -= a;
if (r == 0)
return f;
}
}
return f - r;
}
void addEdge(int u, int v, int c) {
g[u].push_back(e.size());
e.emplace_back(v, c);
g[v].push_back(e.size());
e.emplace_back(u, 0);
}
int maxFlow(int s, int t) {
int ans = 0;
while (bfs(s, t)) {
cur.assign(n, 0);
ans += dfs(s, t, INF);
}
return ans;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
vector<vector<int>> a(n + 1);
for (int i = 1; i <= n; i++) {
int t;
cin >> t;
while (t--) {
int tt;
cin >> tt;
a[i].push_back(tt + n);
}
}
int s1 = n + m + 2, s2 = n + m + 3, s3 = n + m + 4;
int t1 = n + m + 1;
Flow flow(n + 1 + m + 1 + 5);
for (int i = 1; i <= n; i++) {
flow.addEdge(s2, i, 1);
flow.addEdge(s3, i, 1);
for (auto u : a[i]) {
flow.addEdge(i, u, 1);
}
}
for (int i = 1; i <= m; i++) {
flow.addEdge(n + i, t1, 1);
}
flow.addEdge(s1, s2, n);
flow.addEdge(s1, s3, k);
int ans = flow.maxFlow(s1, t1);
cout << ans << '\n';
return 0;
}
J. Prime Game(数学,质因数分解)
分别计算每个质因子对答案的贡献就好了。
假如当前遍历下标为\(i(0-index)\),当前数有一个质因子为2,我们就找到质因子2上一次出现的位置\(pos\),然后当前位置的2对于答案有贡献的区间数量就是\((i-pos)*(n-i)\)。
注意!!
在质因数分解时,加上当前数已经是质数时跳出循环的判断会大幅优化运行时间!!
质因数分解代码
int x = a[i];
for (auto xx : prime) {
if (!vis[x]) { //加上此判断,时间大幅优化
cnt.push_back(x);
break;
}
if (x == 1) break;
if (x % xx == 0) cnt.push_back(xx);
while (x % xx == 0) {
x /= xx;
}
}
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int N = 1e6 + 23;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
vector<int> prime, vis(N);
vis[0] = vis[1] = true;
for (ll i = 2; i <= 1e6; i++) {
if (!vis[i]) {
prime.push_back(i);
for (ll j = i * i; j <= 1e6; j += i) {
vis[j] = true;
}
}
}
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
ll ans = 0;
vector<int> mp(N, -1);
for (int i = 0; i < n; i++) {
vector<int> cnt;
int x = a[i];
for (auto xx : prime) {
if (!vis[x]) {
cnt.push_back(x);
break;
}
if (x == 1) break;
if (x % xx == 0) cnt.push_back(xx);
while (x % xx == 0) {
x /= xx;
}
}
for (auto xx : cnt) {
int pos = mp[xx];
ans += ((ll)i - pos) * (n - i);
mp[xx] = i;
}
}
cout << ans << '\n';
return 0;
}
K. Kangaroo Puzzle(搜索,随机化)
先随便找一个有袋鼠的点作为起点,然后BFS搜索到所有其他有袋鼠的点的路径并把全部路径反向存起来,每个字符串代表某个有袋鼠的点到起点的路径,于是我们现在只需要按一定顺序输出刚刚存起来的字符串就可以让所有袋鼠集合了。
我们首先想到的可能是让离起点远的袋鼠先走到起点,但这样其实不能保证让其他袋鼠走到起点时不会让已走到起点过的袋鼠回到原来的位置。
具体顺序是,每个字符串按它每个方向出现的数量存进一个递减map,比如
"UUUUDDDLLR",就分别存进map[4],map[3],map[2],map[1]。
最后再遍历一次map,同时按字符串长度由大到小输出就好了。
证明:对于方向'U'来说,当前输出的字符串内含有’U‘的数量一定比前一次输出方向'D'的数量少,其他方向同理,所以一定能保证袋鼠不会回到原来位置。
其实,由于题目输出限制很宽,且袋鼠一定是不断合并的,所以随机输出方向乱走也行。
代码(搜索)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
string ds = "DURL";
struct node {
string sp;
int x, y;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
int s = 0, t = 0;
vector a(n + 2, vector<int>(m + 2));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
char ch;
cin >> ch;
a[i][j] = ch - '0';
if (a[i][j] == 1) {
s = i, t = j;
}
}
}
vector<string> ans;
vector vis(n + 2, vector<int>(m + 2));
queue<node> q;
q.push({"", s, t});
vis[s][t] = true;
while (!q.empty()) {
auto [sp, x, y] = q.front();
q.pop();
ans.push_back(sp);
for (int i = 0; i < 4; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (!vis[xx][yy] && xx >= 1 && xx <= n && yy >= 1 && yy <= m && a[xx][yy] == 1) {
vis[xx][yy] = true;
q.push({sp + ds[i], xx, yy});
}
}
}
map<int, vector<string>, greater<>> mp;
for (int i = ans.size() - 1; i >= 1; i--) {
reverse(ans[i].begin(), ans[i].end());
mp[count(ans[i].begin(), ans[i].end(), 'U')].push_back(ans[i]);
mp[count(ans[i].begin(), ans[i].end(), 'D')].push_back(ans[i]);
mp[count(ans[i].begin(), ans[i].end(), 'L')].push_back(ans[i]);
mp[count(ans[i].begin(), ans[i].end(), 'R')].push_back(ans[i]);
}
for (auto [x, y] : mp) {
for (auto xx : y) {
cout << xx;
}
}
return 0;
}
代码(随机化)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
string ds = "UDLR";
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
mt19937 rnd(time(nullptr));
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++) {
string s;
cin >> s;
}
for (int i = 0; i < 50000; i++) {
cout << ds[rnd() % 4];
}
return 0;
}
M. Mediocre String Problem(Z函数,manacher)
题意:三个下标\(i,j,k\),\(j-i+1>k\),使\(s[i,j]+t[0,k]\)为回文串,有多少种可能。
容易知道,\(t[0,k]\)肯定是\(s[i,i+k]\)的翻转,所以我们如果把s串整个反转后,题目就变成了在s中找t的前缀,而这正是Z函数所能做到的。
容易知道,\(s[i+k+1,j]\)肯定是个回文串,因为要使(S + SS + S的翻转)为回文串,那么在中间的SS肯定也是个回文串,所以我们可以用manacher找出以\(i+k+1\)为开头有多少个回文串,这个用manacher求出\(r[i]\)之后再差分前缀和一下就可以得到了!
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
vector<int> z_function(string& s) {
int n = (int)s.length();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0, r - i + 1);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
return z;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s, t;
cin >> s >> t;
const int n = s.size(), m = t.size();
string rs = s;
reverse(rs.begin(), rs.end());
string ts = t + "?" + rs;
vector<int> z = z_function(ts);
vector<int> len(n);
for (int i = m + 1; i < ts.size(); i++) {
int j = i - m - 1;
int k = n - 1 - j;
len[k] = z[i];
}
string mt = "#";
for (auto c : s) {
mt += c;
mt += '#';
}
vector<int> r(2 * n + 1);
for (int i = 0, j = 0; i < 2 * n + 1; i++) {
if (2 * j - i >= 0) {
r[i] = max(0, min(j + r[j] - i, r[2 * j - i]));
}
while (i - r[i] >= 0 && i + r[i] < 2 * n + 1 && mt[i - r[i]] == mt[i + r[i]]) {
r[i]++;
}
if (i + r[i] > j + r[j]) {
j = i;
}
}
vector<int> pre(n + 1);
for (int i = 0; i < 2 * n + 1; i++) {
int L = (i - r[i] + 1) / 2;
int R = (i + r[i] - 1) / 2;
pre[L]++, pre[(i + 1) / 2]--;
}
for (int i = 1; i < n; i++) {
pre[i] += pre[i - 1];
}
ll ans = 0;
for (int i = 0; i < n - 1; i++) {
ans += (ll)len[i] * pre[i + 1]; //len[i]为与t前缀匹配的长度,pre[i+1]为以i+1为头的回文串数量
}
cout << ans << '\n';
return 0;
}