2020 ICPC Asia Kunming Regional Contest Solutions
C. Cities
题意
有一个长度为$n$的序列,可以选择一段连续且数字相同的序列改整体改变为其它数字,问最少多少次操作可以将整个序列变得相同。
思路
挺显然的区间$DP$。
首先对序列$a$去重,即将连续且相同的序列长度变为$1$即可,现在的序列$a$肯定是相邻之间不相同。
如果去重后的序列$a$两两不相同,那答案就是序列的长度。
如果有相同的,如何减少操作次数?
如果$a[l] == a[r]$,那么可以让区间$[l, r]$变成和$a[l]$一样的,这样就减少了一次操作,所以这一题可以从两边端点一样的区间入手减少操作次数。
令$dp[l][r]$表示将区间$[l, r]$变成一样的最少操作次数,$pre[i]$表示左边上一次出现$a[i]$的位置。
状态转移方程:
- 用$dp[l + 1][r] + 1$和$dp[l][r - 1] + 1$更新
- 如果$a[l] == a[r]$,可以用$dp[l + 1][r - 1] + 1$更新
- 对于区间$[l + 1, r - 1]$中的位置$k$, 如果满足$a[k] == a[r]$,则可以用$dp[l][k - 1] + dp[k][r] + 1$更新
- 对于区间$[l + 1, r - 1]$中的位置$k$, 如果满足$a[l] == a[k] == a[r]$,则可以用$dp[l][k - 1] + dp[k + 1][r]$更新
最后的答案就是$dp[1][n]$。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e3 + 10;
int n;
int a[maxn], pre[maxn];
int dp[maxn][maxn];
int dfs(int l, int r) {
if (l >= r) return 0;
if (dp[l][r] != -1) return dp[l][r];
int ans = (r - l + 1) - 1;
ans = min(ans, dfs(l + 1, r) + 1);
ans = min(ans, dfs(l, r - 1) + 1);
if (a[l] == a[r]) ans = min(ans, dfs(l + 1, r - 1) + 1);
for (int k = pre[r]; k > l; k = pre[k]) {
ans = min(ans, dfs(l, k - 1) + dfs(k, r) + 1);
if (a[l] == a[r]) ans = min(ans, dfs(l, k - 1) + dfs(k + 1, r));
}
dp[l][r] = ans;
return ans;
}
void slove() {
cin >> n;
map<int, int> pos;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] == a[i - 1]) {
i--, n--;
continue;
}
pre[i] = pos[a[i]];
pos[a[i]] = i;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = -1;
cout << dfs(1, n) << '\n';
}
int main() {
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(0);
int T;
cin >> T;
while (T--) slove();
return 0;
}
H. Hard Calculation
题意
昆明区域赛每一年都会举办,第一年是2021年,问第x届在哪一年举办。
思路
输出$2020 + x$即可
Code
#include <bits/stdc++.h>
using namespace std;
int main() {
int x;
cin >> x;
cout << x + 2020 << '\n';
return 0;
}
I. Mr.main and Windmills
题意
有一个从$s$点到$t$点的火车,沿途的某一边有很多风车,当火车在不同位置观察风车时,某两个风车的左右位置可能会发生变化,问第$h$个风车发生$k$次相对变化时火车在哪个位置。
思路
对于风车$i$,记录下与其他风车连成的直线与线段$s -> t$的交点,并且对这些交点从近到远排序即可。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
struct pt {
double x, y;
pt() {}
pt(double a, double b) : x(a), y(b) {}
pt operator +(pt p) {return pt(x + p.x, y + p.y);}
pt operator -(pt p) {return pt(x - p.x, y - p.y);}
pt operator *(double v) {return pt(x * v, y * v);}
pt operator /(double v) {return pt(x / v, y / v);}
double operator *(pt p) {return x * p.x + y * p.y;}
double operator ^(pt p) {return x * p.y - y * p.x;}
double corss(pt a, pt b) {return (a - *this) ^ (b - *this);}
double dot(pt a, pt b) {return (a - *this) * (b - *this);}
double getlen() {return x * x + y * y;}
};
struct ln {
pt p, q;
ln() {}
ln(pt a, pt b) : p(a), q(b) {}
pt operator *(ln o) {
double h1 = p.corss(o.p, o.q);
double h2 = q.corss(o.q, o.p);
return (p * h2 + q * h1) / (h1 + h2);
}
};
int n, q;
ln r;
pt s[maxn];
vector<pt> ans[maxn];
bool cmp(pt a, pt b) {
return (a - r.p).getlen() < (b - r.p).getlen();
}
int main() {
// freopen("in.txt", "r", stdin);
scanf("%d%d", &n, &q);
scanf("%lf%lf%lf%lf", &r.p.x, &r.p.y, &r.q.x, &r.q.y);
for (int i = 1; i <= n; i++) {
scanf("%lf%lf", &s[i].x, &s[i].y);
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) continue;
pt c = ln(s[i], s[j]) * r;
if (c.dot(r.p, r.q) < 0) ans[i].push_back(c);
}
sort(ans[i].begin(), ans[i].end(), cmp);
}
while (q--) {
int h, k;
scanf("%d%d", &h, &k);
if (k > (int)ans[h].size()) printf("-1\n");
else printf("%.12lf %.12lf\n", ans[h][k - 1].x, ans[h][k - 1].y);
}
return 0;
}
J. Parallel Sort
题意
有一个$n$的排列,每一轮可以选任意多对数字进行交换,且每一轮每一个数字只能被选择一次,问最少需要多少轮操作可以将这个排列变成升序的。
思路
如果$p[i] != i$,即该数字不在它该在的位置时,必然存在环,且每一个环都是独立的,可以在同一轮对所有环进行操作,互不影响。
考虑将$i->p[i]$连边,比如序列$[2, 3, 4, 5, 6, 1]$所构成的的环如下:
考虑交换$[1, 2], [3, 6], [4, 5]$将数列变成$[1, 6, 5, 4, 3, 2]$,则环变成:
发现大环被拆成若干个二元环,则再进行一次交换就可以完全复原。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
int p[maxn], vis[maxn];
vector<pair<int, int>> slove() {
memset(vis, 0, sizeof(vis));
vector<pair<int, int>> res;
for (int i = 1; i <= n; i++) {
if (vis[i] || p[i] == i) continue;
vector<int> v;
int tmpv = i;
while (!vis[tmpv]) {
v.push_back(tmpv);
vis[tmpv] = 1;
tmpv = p[tmpv];
}
int sz = (int)v.size();
for (int l = 0, r = sz - 1; l < r; l++, r--) {
res.push_back({v[l], v[r]});
swap(p[v[l]], p[v[r]]);
}
}
return res;
}
int main() {
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> p[i];
vector<vector<pair<int, int>>> ans;
while (1) {
vector<pair<int, int>> res = slove();
if (res.empty()) break;
ans.push_back(res);
}
cout << (int)ans.size() << '\n';
for (auto v : ans) {
cout << (int)v.size();
for (auto vv : v) cout << " " << vv.first << ' ' << vv.second;
cout << '\n';
}
return 0;
}
K. Riichi!!
题意
麻将题,Riichi!!
思路
模拟
Code
#include <bits/stdc++.h>
using namespace std;
int cnt[40];
string str;
int ok;
vector<int> riichi;
vector<vector<int>> discard;
// 1w-9w 1-9
// 1b-9b 10-18
// 1s-9s 19-27
// 1z-7z 28-34
void check(int cur, int rem) {
if (rem < 0) return;
if (rem == 0) {
ok = 1;
return;
}
if (ok) return;
if (!cnt[cur]) check(cur + 1, rem);
if (cnt[cur] >= 3) {
cnt[cur] -= 3;
check(cur, rem - 3);
cnt[cur] += 3;
}
if (rem % 3 == 2 && cnt[cur] >= 2) {
cnt[cur] -= 2;
check(cur, rem - 2);
cnt[cur] += 2;
}
if (cur <= 27 && cur % 9 != 0 && cur % 9 != 8 && cnt[cur] && cnt[cur + 1] && cnt[cur + 2]) {
--cnt[cur], --cnt[cur + 1], --cnt[cur + 2];
check(cur, rem - 3);
++cnt[cur], ++cnt[cur + 1], ++cnt[cur + 2];
}
}
string get_str(int x) {
string s = "";
if (x <= 9) {
s = "w";
s = (char)(x + '0') + s;
} else if (x <= 18) {
s = "b";
s = (char)(x - 9 + '0') + s;
} else if (x <= 27) {
s = "s";
s = (char)(x - 18 + '0') + s;
} else {
s = 'z';
s = (char)(x - 27 + '0') + s;
}
return s;
}
int main() {
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(0);
int T;
cin >> T;
while (T--) {
memset(cnt, 0, sizeof(cnt));
cin >> str;
for (int i = 0; i < 28; i += 2) {
if (str[i + 1] == 'w') ++cnt[str[i] - '0'];
else if (str[i + 1] == 'b') ++cnt[str[i] - '0' + 9];
else if (str[i + 1] == 's') ++cnt[str[i] - '0' + 18];
else ++cnt[str[i] - '0' + 27];
}
ok = 0;
check(1, 14);
if (ok) {
cout << "Tsumo!\n";
continue;
}
riichi.clear();
discard.clear();
for (int i = 1; i <= 34; ++i) {
if (cnt[i]) {
--cnt[i];
vector<int> tmp;
for (int j = 1; j <= 34; ++j) {
if (i == j) continue;
++cnt[j];
ok = 0;
check(1, 14);
if (ok) {
tmp.push_back(j);
}
cnt[j]--;
}
if (!tmp.empty()) {
riichi.push_back(i);
discard.push_back(tmp);
}
++cnt[i];
}
}
cout << (int)riichi.size() << '\n';
for (int i = 0; i < (int)riichi.size(); ++i) {
cout << get_str(riichi[i]) << ' ';
for (int j = 0; j < (int)discard[i].size(); ++j)
cout << get_str(discard[i][j]);
cout << '\n';
}
}
return 0;
}
L. Simone and graph coloring
题意
有一个长度为$n$的排列,对于每一对逆序对,都会有一条无向边相连,现在要为每一个点染色,相邻的点颜色不能相同,问最少需要多少种颜色,并且输出每个点的染色情况。
思路
很显然,最少需要的颜色数与最长下降子序列的长度相同。考虑从右到左进行染色,对最后面一个染颜色$1$,对第$n - 1$个数开始,找离它最近的比它小的数的下标$idx$,则$col[i] = col[idx] + 1$,如果没有比它小的数的话,则$col[i] = 1$。这一个过程与$DP$求解$LIS$时一模一样。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int a[maxn];
int ans[maxn];
void slove() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
map<int, int> mp;
for (int i = 1; i <= n; i++) mp[a[i]] = i;
vector<int> seg;
seg.push_back(a[n]);
ans[n] = 1;
for (int i = n - 1; i >= 1; i--) {
auto pos = lower_bound(seg.begin(), seg.end(), a[i]);
if (pos == seg.end()) {
ans[i] = ans[mp[seg.back()]] + 1;
seg.push_back(a[i]);
} else {
if (pos - seg.begin() == 0) ans[i] = 1;
else ans[i] = ans[mp[seg[pos - seg.begin() - 1]]] + 1;
seg[pos - seg.begin()] = a[i];
}
}
printf("%d\n", (int)seg.size());
for (int i = 1; i <= n; i++) {
if (i > 1) printf(" ");
printf("%d", ans[i]);
}
printf("\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) slove();
return 0;
}
M. Stone Games
题意
给一个长度为$n$的序列,有$Q$次询问,询问给定一个区间$[L, R]$,问该区间的子集和无法组成的最小数是什么,询问强制在线。
思路
- 首先判断这个区间有没有$1$,没有的话就是$1$。
- 假设我们已知能够组成区间$[1, x]$内的所有数,区间$[L, R]$中属于$[1, x + 1]$的数的和为$sum$,则我们能够组成区间$[1, sum + 1]$内的所有数,若$sum == x$,则表明我们无法组成$x + 1$,否则就让$x = sum$,继续扩大$x$,去寻找答案。
- 我们需要知道区间$[L, R]$中属于区间$[x, y]$的数的和,可以用主席树来维护。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 10;
const int inf = 1e9 + 10;
struct Node {
int l, r;
ll sum;
} tree[maxn * 60];
int n, q, tot;
int rt[maxn];
void update(int &x, int y, int l, int r, int v) {
tree[x = ++tot] = tree[y];
tree[x].sum += v;
if (l == r) return;
int mid = (l + r) >> 1;
if (v <= mid) update(tree[x].l, tree[y].l, l, mid, v);
if (v > mid) update(tree[x].r, tree[y].r, mid + 1, r, v);
}
ll query(int st, int ed, int l, int r, int x, int y) {
if (st <= l && r <= ed) return tree[y].sum - tree[x].sum;
int mid = (l + r) >> 1;
ll res = 0;
if (st <= mid) res += query(st, ed, l, mid, tree[x].l, tree[y].l);
if (ed > mid) res += query(st, ed, mid + 1, r, tree[x].r, tree[y].r);
return res;
}
int main() {
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
update(rt[i], rt[i - 1], 1, inf, x);
}
ll ans = 0;
while (q--) {
int l, r;
cin >> l >> r;
l = (l + ans) % n + 1;
r = (r + ans) % n + 1;
if (l > r) swap(l, r);
ll sum = 0;
while (1) {
ll tmp = query(1, min(sum + 1, 1ll * inf), 1, inf, rt[l - 1], rt[r]);
if (tmp == sum) break;
sum = tmp;
}
ans = sum + 1;
cout << ans << '\n';
}
return 0;
}