
1차 예선에서 1, 2, 3, 4번을 풀어서 800점 만점에 600점을 받았습니다. 부분 점수는 안 못 받았습니다.
다음 날 수술이 있었고, 4번까지 풀고 나니 새벽이어서 5번은 안 풀고 잤습니다. 전체적으로 디스크립션이 난해한 느낌이라 안타까웠습니다.
제 1차 1, 2, 3, 4번 풀이를 공유합니다.
1차 1번 – 다이어트
A 식당과 B 식당이 있고, 이 식당에는 각각 개의 메뉴가 있습니다. 이 메뉴들의 칼로리 값들이 주어집니다. 일동안 매일 A에서 한 끼를, B에서 한 끼를 먹을 때, 하루에 먹은 칼로리의 양의 최댓값이 최소가 되게 하고 싶습니다.
A 식당에서의 최대 칼로리의 음식을 순서대로, B 식당에서 최소 칼로리의 음식을 순서대로 일간 하나하나 그리디하게 매칭해 주면 됩니다.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using uint = unsigned int;
using ull = unsigned long long;
using pii = pair<int, int>;
/* [t] [c] [s] */
int a[200000], b[200000];
int main() {
cin.tie(nullptr);
cout.tie(nullptr);
ios_base::sync_with_stdio(false);
int tt;
cin >> tt;
for (int _ = 1; _ <= tt; _++) {
cout << "Case #" << _ << "\n";
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
for (int i = 0; i < n; i++) cin >> b[i];
sort(b, b + n);
int mx = 0;
for (int i = 0; i < k; i++) mx = max(mx, a[i] + b[k - i - 1]);
cout << mx << '\n';
}
return 0;
}1차 2번 – 카드 게임
개의 카드가 쌓여 있는 두 덱 X와 Y가 있습니다. 각 카드에는 이하의 양의 정수가 적혀 있습니다. 두 명이 번갈아 카드를 가져가는데,
꼭 한 개의 덱을 선택해서 위에서부터 한 장 이상을 가져가야 하며,
가져간 카드의 합이 이하여야 하고,
마지막으로 카드를 가져가는 사람이 집니다.
내 턴에 덱 X에 장의 카드가, 덱 Y에 장의 카드가 남아 있는 상태를 라고 한다면, 과 가 정해져 있는 게임에 대해 중간 상태로 가능한 경우는 총 개입니다. 각각의 상태에서 두 플레이어가 각각 최선의 방법으로 플레이했을 때 내가 반드시 이기는 경우와 반드시 지는 경우의 갯수를 세고 싶습니다.
Grundy number를 바로 떠올릴 수 있겠지만, 아쉽게도 마지막으로 카드를 가져가는 사람이 지는 게임(misère)이기 때문에 Grundy를 쓸 수 없습니다. Grundy로 접근하기 전에 ‘이게 1차 예선 2번에 나올 수 있을 만한 지식인가?’를 생각해 볼 필요가 있습니다.
대신 다이나믹 프로그래밍으로 접근할 수 있습니다.
현재 상태 에서 내가 어떤 덱에서 얼마나 많이 가져가더라도 다음 플레이어가 이기는 상태만 나온다면, 현재 상태는 내가 지는 상태입니다.
그렇지 않다면, 내가 특정한 동작을 하면 다음 플레이어가 지는 상태를 만들 수 있습니다. 이런 경우 현재 상태는 내가 이기는 상태입니다.
따라서, DP 테이블을 만들어둔 후 가져간 카드의 합이 이하가 되도록 X에서 하나씩, Y에서 하나씩 가져가면서 다음 플레이어가 이기는 상태가 있는지 확인해 주면 됩니다. 이 계산을 개에 대해 하므로, 입니다.
이므로 Large 셋을 풀기에는 불충분합니다. 이는 Prefix sum을 이용해 로 최적화할 수 있습니다.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using uint = unsigned int;
using ull = unsigned long long;
using pii = pair<int, int>;
/* [t] [c] [s] */
int x[3010], y[3010], xp[3010], yp[3010], xi[3010], yj[3010];
bitset<3010> dp[3010];
int prs[3010][3010];
int main() {
cin.tie(nullptr);
cout.tie(nullptr);
ios_base::sync_with_stdio(false);
int tt;
cin >> tt;
for (int _ = 1; _ <= tt; _++) {
cout << "Case #" << _ << "\n";
memset(dp, 0, sizeof dp);
dp[0][0] = true;
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> x[i], xp[i] = x[i] + xp[i - 1];
int v = xp[i] - k - 1;
int ii = upper_bound(xp, xp + i, v) - xp;
xi[i] = ii;
}
for (int j = 1; j <= n; j++) {
cin >> y[j], yp[j] = y[j] + yp[j - 1];
int v = yp[j] - k - 1;
int jj = upper_bound(yp, yp + j, v) - yp;
yj[j] = jj;
}
int a = 0, b = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
int xc = (prs[i][j + 1] - prs[xi[i]][j + 1] - prs[i][j] + prs[xi[i]][j]);
int yc = (prs[i + 1][j] - prs[i + 1][yj[j]] - prs[i][j] + prs[i][yj[j]]);
dp[i][j] = ((i - xi[i]) - xc) || ((j - yj[j]) - yc);
dp[0][0] = true;
(dp[i][j] ? a : b)++;
prs[i + 1][j + 1] = dp[i][j] + prs[i][j + 1] + prs[i + 1][j] - prs[i][j];
}
}
cout << prs[n + 1][n + 1] << ' ' << (n + 1) * (n + 1) - prs[n + 1][n + 1] << endl;
}
return 0;
}1차 3번 – 사다리 게임
개의 세로선과 개의 가로선이 있는 사다리가 있습니다.
개의 쿼리가 들어옵니다. 의 형태입니다. 위의 번 위치에서 시작해서 아래의 번 위치에서 끝나도록 가로선을 적절히 없애 사다리를 조작하고 싶은데, 가로선을 최소한으로 없앴을 때 몇 개 없애야 하는지를 구해야 합니다.
Kotlin Heroes: Episode 4의 E번 문제와 상당히 비슷합니다.
가로선들이 그래프의 간선들이라고 생각하고 접근해서 최단 거리 문제로 해결했습니다.
와 를 잇는 가로선이 있다면, 가로선을 무시하는 경우(, )를 가중치 로, 무시하지 않는 경우(, )를 가중치 으로 하여 간선 개로 만들어 줍니다.
정점은 개를 처음에 만들어 주고, 가로선을 하나 처리할 때마다 가로선의 끝점에 각각 하나씩, 총 개를 더 만들어 줍니다. 이렇게 그래프를 구성하면 노드는 총 개가 됩니다.
이렇게 하면 각 시작점마다 Dijkstra 혹은 0-1 BFS를 돌리는 것으로 각 종점까지의 최단거리를 구하면, 그것이 시작점에서 종점까지 가면서 제거해야 하는 가로선의 개수의 최댓값이 됩니다. 이 방법을 이용해 쿼리 값들을 전부 전처리해둔 후, 쿼리가 들어오는 대로 전처리된 값들을 출력해 주려고 합니다.
만들어 준 그래프의 정점은 개, 간선은 개이므로 0-1 BFS의 경우 한 번에 , Dijkstra의 경우 한 번에 입니다. 이를 모든 시작점에 대해 한 번씩 돌려주면 0-1 BFS의 경우 , Dijkstra의 경우 만에 해결할 수 있습니다. 저는 그냥 Dijkstra로 해결했습니다.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using uint = unsigned int;
using ull = unsigned long long;
using pii = pair<int, int>;
/* [t] [c] [s] */
pii seg[2000];
int last[1500], dist[1500][1500], dtemp[9000];
bitset<9000> vis;
void dijk(int s, const vector<vector<pii>> &graph) {
memset(dtemp, -1, sizeof dtemp);
vis.reset();
priority_queue<pii> pq;
dtemp[s] = 0;
pq.emplace(0, s);
while (pq.size()) {
int u = pq.top().second, d = -pq.top().first;
pq.pop();
if (dtemp[u] < d) continue;
if (vis[u]) continue;
vis[u] = true;
for (const auto &kv : graph[u]) {
int v = kv.first, c = kv.second;
if (dtemp[v] == -1 || dtemp[u] + c < dtemp[v]) {
dtemp[v] = dtemp[u] + c;
pq.emplace(-dtemp[v], v);
}
}
}
}
int main() {
cin.tie(nullptr);
cout.tie(nullptr);
ios_base::sync_with_stdio(false);
int tt;
cin >> tt;
for (int _ = 1; _ <= tt; _++) {
cout << "Case #" << _ << "\n";
memset(dist, -1, sizeof dist);
int n, k, q;
cin >> n >> k >> q;
vector<vector<pii>> graph(n);
for (int i = 0; i < n; i++) last[i] = i;
for (int i = 0; i < k; i++) {
cin >> seg[i].first >> seg[i].second;
seg[i].first--, seg[i].second--;
}
for (int i = 0; i < k; i++) {
const auto &l = seg[i];
int gn = graph.size();
graph[last[l.first]].emplace_back(gn, 1);
graph[last[l.second]].emplace_back(gn + 1, 1);
graph[last[l.first]].emplace_back(gn + 1, 0);
graph[last[l.second]].emplace_back(gn, 0);
last[l.first] = gn;
last[l.second] = gn + 1;
graph.emplace_back();
graph.emplace_back();
}
for (int i = 0; i < n; i++) {
graph[last[i]].emplace_back(graph.size(), 0);
last[i] = graph.size();
graph.emplace_back();
}
for (int i = 0; i < n; i++) {
dijk(i, graph);
for (int j = 0; j < n; j++) dist[i][j] = dtemp[last[j]];
}
int s = 0;
while (q--) {
int u, v;
cin >> u >> v;
u--, v--;
s += dist[u][v];
}
cout << s << endl;
}
return 0;
}1차 4번 – 범위 안의 숫자
0에서 9까지의 숫자 개로 이루어진 문자열 가 주어집니다. 길이가 인 부분 문자열들을 수라고 생각하고, 모두 수직선 위에 둡니다.
이 때, 여기서 최대 한 글자까지를 1로 바꿔서, 어떤 정수 에 대해 길이가 인 구간 안에 속한 수의 개수의 최댓값이 최대가 되게 하고 싶습니다. 이 때의 최댓값을 구해야 합니다.
일단 가 커봐야 고, 도 그렇게 크지 않아서, 문자 하나를 1로 바꿀 수 있다는 것을 배제하고 생각하면 이 문제는 스위핑으로 해결할 수 있습니다. 부분 문자열의 값을 라고 하면, 선분 들을 수직선 위에 깔아 두고 가장 많이 겹쳤을 때의 선분의 갯수를 구하면 됩니다.
이제 문자 하나를 1로 바꿀 수 있는 경우를 생각해봅시다. ‘문자 하나를 1로 바꾸는 것’이라는 행위에서 관찰 가능한 특별한 성질은 딱히 없다고 느꼈습니다. 다만, 문자 하나를 1로 바꾸면 다시 만들어줘야 하는 선분의 개수는 개에 불과하다는 사실에 집중했습니다.
만약 다이나믹하게 선분을 제거하고 추가할 수 있는 스위핑이 있다면, 모든 위치의 숫자를 하나하나 1로 바꾸고 되돌리는 행위를 한다고 해도 추가/제거는 번만 일어나므로 시간 안에 해결할 수 있을 것 같습니다.
다행히도 그런 스위핑은 존재하는데요, 최댓값을 구하는 연산과 구간에 덧셈을 하는 연산(lazy propagation 등)을 수행 가능한 세그먼트 트리로 가능합니다.
다만 인덱스가 꽤 크기 때문에 다이나믹 세그먼트 트리를 구현하거나, 가능한 값들을 전부 좌표 압축한 후 처리하는 방법을 사용해야 합니다. 저는 후자로 구현했습니다. 총 시간 복잡도는 입니다.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using uint = unsigned int;
using ull = unsigned long long;
using pii = pair<int, int>;
/* [t] [c] [s] */
ll tree[1048576 * 2], lazy[1048576 * 2];
int c;
void _propagate(int x, int s, int e) {
if (!lazy[x]) return;
tree[x] += lazy[x];
if (s != e) {
lazy[x * 2] += lazy[x];
lazy[x * 2 + 1] += lazy[x];
}
lazy[x] = 0;
}
ll _maximum(int x, int s, int e, int l, int r) {
_propagate(x, s, e);
if (l > e || r < s) return 0;
if (l <= s && e <= r) return tree[x];
int m = (s + e) / 2;
return max(_maximum(x * 2, s, m, l, r),
_maximum(x * 2 + 1, m + 1, e, l, r));
}
void _update(int x, int s, int e, int l, int r, ll dv) {
_propagate(x, s, e);
if (l > e || r < s) return;
if (l <= s && e <= r) {
tree[x] += dv;
if (s != e) {
lazy[x * 2] += dv;
lazy[x * 2 + 1] += dv;
}
return;
}
int m = (s + e) / 2;
_update(x * 2, s, m, l, r, dv);
_update(x * 2 + 1, m + 1, e, l, r, dv);
tree[x] = max(tree[x * 2], tree[x * 2 + 1]);
}
ll maximum(int l, int r) {
return _maximum(1, 0, c - 1, l, r);
}
void update(int l, int r, ll dv) {
_update(1, 0, c - 1, l, r, dv);
}
ll zip(ll x, const vector<ll> &a) {
return lower_bound(a.begin(), a.end(), x) - a.begin();
}
ll v[50000], v1[50000][10];
int main() {
cin.tie(nullptr);
cout.tie(nullptr);
ios_base::sync_with_stdio(false);
int tt;
cin >> tt;
for (int _ = 1; _ <= tt; _++) {
cout << "Case #" << _ << "\n";
ll n, k, m;
cin >> n >> k >> m;
string t;
cin >> t;
// init values
vector<ll> ps;
ll curr = 0, mod = 1;
for (int i = 0; i < k; i++) mod *= 10;
for (int i = 0; i < k; i++) curr *= 10, curr += t[i] & 15, curr %= mod;
for (int i = k; i <= n; i++) {
v[i - k] = curr;
ps.emplace_back(curr);
ll l = curr, r = 0, p = 1;
for (int j = 0; j < k; j++) {
v1[i - k][j] = (l / 10 * 10 + 1) * p + r;
ps.emplace_back(v1[i - k][j]);
l /= 10, r += (curr / p) % 10 * p, p *= 10;
}
if (i == n) break;
curr *= 10, curr += t[i] & 15, curr %= mod;
}
int ts = ps.size();
for (int i = 0; i < ts; i++) ps.emplace_back(ps[i] + m);
sort(ps.begin(), ps.end());
ps.erase(unique(ps.begin(), ps.end()), ps.end());
memset(tree, 0, sizeof tree);
memset(lazy, 0, sizeof lazy);
c = ps.size();
// query
for (int i = 0; i <= n - k; i++) {
update(zip(v[i], ps), zip(v[i] + m, ps), 1);
}
ll mx = maximum(0, ps.size() - 1);
for (int i = 0; i < n; i++) { // replace i-th number to 1
// remove
for (int j = 0; j < k; j++) {
if (i - k + j < 0 || i - k + j > n - k) continue;
update(zip(v1[i - k + j][j], ps), zip(v1[i - k + j][j] + m, ps), -1);
}
if (i <= n - k) {
update(zip(v[i], ps), zip(v[i] + m, ps), -1);
}
// add
if (i - k >= 0) {
update(zip(v[i - k], ps), zip(v[i - k] + m, ps), 1);
}
for (int j = 0; j < k; j++) {
if (i - k + 1 + j < 0 || i - k + 1 + j > n - k) continue;
update(zip(v1[i - k + 1 + j][j], ps), zip(v1[i - k + 1 + j][j] + m, ps), 1);
}
// query
mx = max(mx, maximum(0, ps.size() - 1));
}
cout << mx << endl;
}
return 0;
}
5 comments
aararaarar
🤐
와, 멋지십니다. 귀중한 코디 써주셔서 감사합니다. 저도 뜯어보면서 열심히 배우겠습니다~ ^^
트위터에서 주소 공유해 주신 거 보고 있습니다. 정말 많은 공부가 되고 있습니다. 저에게는 아직도 까마득~~~히 먼 길이네요. 소중한 자료 공유해주심에 다시 한 번 감사드립니다.
글 읽어 주셔서 감사합니다. 도움이 되셨다니 기쁩니다 😄
Posts mentioning this