PS/My Diary (PS, 대회후기)

[그룹연습] Happy New Year! Div.1 후기

kth990303 2021. 2. 14. 16:41
반응형

블로그 쓰는게 너무 귀찮아서 여태껏 안쓰다가, 백준 게시판에 코드블럭 처리하는게 너무 마음에 안들어서 티스토리 블로그에 쓰게 됐습니다.

대회는 총 2시간, 6문항으로 진행됐으며, C번이 진짜 이런 문제일 줄 몰랐습니다.

풀면서 아... div.1 문제 선정이 잘못됐구나 느꼈죠.

2시간 6문항이라 일부러 빡구현은 넣지 않길 바랬는데.

 

바로 후기 들어갈게요


건국대 백준 푸는 그룹: 소박하지만 그룹입니다 전용 연습 후기입니다. 

실제 대회 및 모의대회가 아닌, 그룹 내에서 작게 운영하는 연습용 대회이며, 실제 대회보다도 난이도가 낮습니다.



A. 4375번_ 1 (Silver III)

알고리즘 분류: 수학, 브루트포스, 정수론

 

string으로 1을 이어서 붙이는 걸로 하면, 런타임 에러(OutOfBounds)가 뜨는데, 이유를 모르겠다.

짜증나서 한 3번째부터 int로 바꾸기만 해서 제출했는데 AC를 받았다. 

B번 먼저 풀고 다시 A번으로 돌아와서 AC받은 코드.

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int MAX = 10001;
ll N;
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	// 왜 string으로 하면 런타임 에러가 뜨는지 모르겠다
	while (cin >> N) {
		// n은 N으로 나눈 나머지.
		ll n = 1 % N;
		for (int i = 1;; i++) {
			// 나머지가 0이면 배수인 셈
			if (!n) {
				cout << i << "\n";
				break;
			}
			// 1을 이어서 붙여서 %N을 구해준다.
			n *= 10;
			n++;
			n %= N;
		}
	}
}

 


B. 17609번_ 회문 (Silver I)

알고리즘 분류: 구현, 그리디, 문자열, 투포인터

 

이 문제, 나는 왜이리 코드가 더러울까. 11%에서 맞왜틀을 계속 해서 너무 짜증났던 문제.

아래 반례가 큰 도움이 됐다. aaabaabaa는 aabaabaa로 유사 회문이 가능하다

1
aaabaabaa
ans: 1

아무튼 내가 쓴 코드는 아래와 같다. 실1 답지 않은 엄청난 구현... 더 간단하게 풀 수 있을 듯 하다.

// 210214 #17609 회문 Silver I
// 구현, 투포인터 느낌
// 재귀로 더 간단히 풀 수 있는 듯 하다
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
int check(string s) {
	// flag이면 회문, flag2이면 유사회문
	bool flag = true, flag2 = true;
	// del은 삭제 횟수
	int tmpi = 0, tmpj = 0, del = 0;
	for (int i = 0, j = 0;; i++, j++) {
		// 맞왜틀해서 추가한 코드
		if (i > s.length() - 1 - j)
			break;
		// 만약 일치하지 않는 문자가 나타난다면
		if (s[i] != s[s.length() - 1 - j]) {
			// 회문은 아니다
			flag = false;
			// 삭제를 2번 이상 했다면 유사회문도 아니다
			if (del==2) {
				flag2 = false;
				break;
			}
			// 삭제를 1번 했는데, 안맞는 경우가 나온다고
			// 바로 flag2=false로 하면 안된다.
			// 그 이유는 밑 else문에서 알 수 있다
			else if (del==1) {
				i = tmpi;
				j = tmpj;
				del++;
				if (s[i + 1] == s[s.length() - 1 - j]) {
					i++;
					continue;
				}
			}
			else {
				// 삭제를 해야 한다
				del++;
				// 만약 끝에서 j번째 문자를 삭제할 때
				// 문자가 일치한다면 그 경우를 삭제한다.

				// 근데 이 경우에서 일치한다 하더라도,
				// 이 경우로 진행하면 2번 이상 삭제해야 돼서 불가능하면서
				// i번째에서 삭제하는 게 유사회문이 가능한 경우가 있다
				// 따라서 이 경우가 실패할 경우, 그 밑의 else if문이 실행되도록 해야 한다.
				// 따라서 위에서 del==1일 때 처리해주는 코드가 있는 것.
				if (s[i] == s[s.length() - 1 - (j + 1)]) {
					tmpi = i;
					tmpj = j;
					j++;
					continue;
				}
				else if (s[i + 1] == s[s.length() - 1 - j]) {
					tmpi = i;
					tmpj = j;
					i++;
					continue;
				}
				// 둘 다 만족하지 않는다면 그냥 유사회문이 아닌 것
				flag2 = false;
				break;
			}
		}
	}
	if (flag)
		return 0;
	else if (flag2)
		return 1;
	return 2;
}
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);

	int t;
	cin >> t;
	while (t--) {
		string s;
		cin >> s;
		cout << check(s) << "\n";
	}
}

 

그리고 채점 현황으로 맞은 사람들 코드를 보면서, 훨씬 더 간단한 코드가 존재함을 깨달았다.

바로 팰린드롬 여부를 함수로 따로 빼내서 코드를 짜는 것인데,

백문이 불여일견. 굉장히 편한 코드를 공개한다.

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
bool check(string s, int left, int right) {
	while (left < right) {
		if (s[left++] != s[right--])
			return false;
	}
	return true;
}
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);

	int t;
	cin >> t;
	while (t--) {
		string s;
		int ans = 0;
		cin >> s;
		for (int i = 0, j = s.length() - 1; i < j; i++, j--) {
			if (s[i] != s[j]) {
				if (check(s, i + 1, j) || check(s, i, j - 1))
					ans = 1;
				else
					ans = 2;
				break;
			}
		}
		cout << ans << "\n";
	}
}

세상에... 정말 보기에도 편하고 풀기에도 좋은 코드이다.

내 코드가 더러웠던 이유는 check(s, i+1, j)랑 check(s, i, j-1)을 or로 체크하지 못하기 때문에 더러운 것이었는데, 

이 코드는 아예 체크하는 부분을 함수로 따로 빼내서 코드를 간단하게 만들었다.

 

그리고, 체크하는 부분을 인자로 넘겨주었다. 필요한 정보가 무엇인지 딱 뽑아내 인자로 정확히 전달하는 능력.

코딩을 잘하려면 필요한 정보, 그리고 최대한 객체지향(...?) 적인 코드가 좋음을 다시 한 번 깨닫게 되는 순간이었다.

 

뭐... 백준에서 객체지향이라 말하니까 웃기긴 한데, 여러번 쓰이는 기능은 최대한 함수로 뽑아내자.

아니더라도, 연습을 통해 필요한 정보는 바로바로 파악할 수 있도록 하자.


C. 5427번_ 불 (Gold IV)

알고리즘 분류: Graph, BFS

 

자, 문제를 보면 상근이가 불을 피해 최대한 빨리 탈출하고 싶어한다.

일단 문제 자체를 보아하니 이동하는 데에 가중치가 없다 + 그래프 느낌이 난다 -> dfs/bfs이겠구나 바로 생각이 들었다. 그리고 최단경로로 이동하고 싶어하는구나 bfs를 써야지! 까지만 생각해놓고, 이 문제... 효자 문제겠구나! 했는데...

응 절대아니야

이 문제... 불의 위치가 여러 개인점, 불도 bfs 방식으로 이동한다는 점 때문에 굉장히 구현이 빡셌다.

// 210214 #5427 불 Gold IV
// BFS + 삼성 sw역량테스트 추천문제 + 구현
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <string>
#include <cstring>
using namespace std;
const int MAX = 1001;
// 나의 좌표
struct Point {
	int x, y;
};
Point me;
// 불 좌표 모음
vector<Point> fire;
int H, W, ans, ntime = -1;
bool flag = true;
int dx[4] = { 0,0,-1,1 };
int dy[4] = { -1,1,0,0 };
char a[MAX][MAX];
bool visit[MAX][MAX];
queue<pair<int, int>> q2;
// 불의 이동
void bfs2() {
	int num = fire.size();
	// 처음엔 불이 num개만 존재하며,
	// 굳이 queue를 쓰지 않고 이동만 하면 됨
	for (int i = 0; i < num; i++) {
		int x = fire[i].x;
		int y = fire[i].y;
		for (int j = 0; j < 4; j++) {
			int nx = x + dx[j];
			int ny = y + dy[j];
			if (nx >= 0 && ny >= 0 && nx < W && ny < H) {
				// 불일 경우 그냥 visit처리 시켜버림
				// 그리고 .인 경우만 불이 이동가능함
				// 처음엔 if(a[ny][nx]!='#')으로만 했는데 MLE...
				if (!visit[ny][nx] && a[ny][nx] == '.') {
					a[ny][nx] = '*';
					visit[ny][nx] = true;
					fire.push_back({ nx, ny });
				}
			}
		}
	}
	// 이제 불의 개수는 늘어남.
	num = fire.size();
}
// 탈출하기 위한 나 자신의 bfs
void bfs(int x, int y) {
	queue<pair<pair<int, int>, int>> q;
	q.push({ { x, y }, 0 });
	visit[y][x] = true;
	while (!q.empty()) {
		int x = q.front().first.first;
		int y = q.front().first.second;
		int time = q.front().second;
		q.pop();
		// 이동 시간이 증가하기 시작했으면
		// 불 이동시키고 탐색 시작
		if (ntime < time) {
			ntime = time;
			bfs2();
		}
		/*
		cout << "\n";
		for (int i = 0; i < H; i++) {
			for (int j = 0; j < W; j++) {
				cout << a[i][j];
			}
			cout << "\n";
		}*/
		for (int i = 0; i < 4; i++) {
			int nx = x + dx[i];
			int ny = y + dy[i];
			if (nx >= 0 && ny >= 0 && nx < W && ny < H) {
				if (!visit[ny][nx]) {
					if (a[ny][nx] == '.') {
						visit[ny][nx] = true;
						q.push({ {nx, ny}, time + 1 });
					}
				}
			}
			// 가장자리에 있는 경우
			// 탈출하면 되므로 time+1을 리턴해줌
			else {
				flag = true;
				ans = time + 1;
				return;
			}
		}
	}
}
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);

	int t;
	cin >> t;
	while (t--) {
		ntime = -1;
		flag = false;
		ans = 0;
		memset(a, false, sizeof(a));
		memset(visit, false, sizeof(visit));
		fire.clear();
		cin >> W >> H;
		for (int i = 0; i < H; i++) {
			string s;
			cin >> s;
			for (int j = 0; j < W; j++) {
				a[i][j] = s[j];
				if (a[i][j] == '@') {
					me.y = i;
					me.x = j;
				}
				// 벽인 경우 어차피 이동 못하므로 visit처리 함
				else if (a[i][j] == '#') {
					visit[i][j] = true;
				}
				// 불인 경우 어차피 이동못하므로 visit처리 함
				else if (a[i][j] == '*') {
					visit[i][j] = true;
					fire.push_back({ j, i });
				}
			}
		}
		bfs(me.x, me.y);
		if (flag)
			cout << ans << "\n";
		else
			cout << "IMPOSSIBLE" << "\n";
	}
}

이런 2200B가 넘는 아름다운 코드를 봤나^^...

한시간이 날라갔다.


D. 2616번_ 소형기관차 (Gold IV)

알고리즘 분류: DP

 

얘는 자꾸 125가 답으로 나와서 좀 짜증났는데, 뭐 여차저차 바꿔서 240이 나와서 제출했는데 맞긴 했다.

 

기관차 3대의 각 위치에 따라 최대 인원수가 달라진다.

브루트포스에서 각 기관차의 위치가 메모이제이션이 되냐에 따라서만 값이 갈라지므로 dp임을 확인할 수 있다. (이거 왜케 어렵게 설명하냐... 아무튼 보통 가지수에 따라 브루트포스 -> 분할정복/dp -> 그리디 로 갈라진다)

기관차가 3대이고, 0부터 N-1까지 저장했으므로 dp(3, N-1)의 값을 출력하면 되게 해줬다.

 

그리고 누적합을 구해줘야되는데, 그 이유는 어차피 최대로 담는 경우가 가장 많이 이용객(이었나?)을 수송하는 경우이기 때문에 누적합을 미리 구해준 후, 최대 수송 인원수만큼 더해줘야 되기 때문이다

// 210214 #2616 소형기관차 Gold IV
// 맞왜틀 계속 했던 dp + prefix_sum
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int MAX = 50001;
int N, M, a[MAX], s[MAX], d[4][MAX];
// top-down 좋아
int dp(int item, int cur) {
	int& ret = d[item][cur];
	if (ret != -1)
		return ret;
	if (!item)
		return ret = 0;
	if (cur < M)
		return ret = s[cur + 1];
	ret = dp(item, cur - 1);
	if (item > 0 && cur - M >= 0) {
		ret = max(ret, dp(item - 1, cur - M) 
			+ s[cur + 1] - s[cur + 1 - M]);
	}
	return ret;
}
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);

	cin >> N;
	for (int i = 0; i < N; i++) {
		cin >> a[i];
		s[i + 1] = s[i] + a[i];
	}
	cin >> M;
	memset(d, -1, sizeof(d));
	cout << dp(3, N - 1) << "\n";
}

여기서부터는 대회가 끝나고 풀어본 문제입니다.


E. 16434번_ 드래곤 앤 던전 (Gold III)

알고리즘 분류: 이분 탐색, 구현

 

이분 탐색은 개뿔... 왜 이분탐색으로 하는지 이해가 1도 안되는 문제이다.

이분탐색이 생각나지도 않고 그냥 문제에서 하라는 대로 구현하면 실버급으로 풀린다.

실버2에 난이도 투표 기여했다. 이 문제, C번정도였으면 큰일날 뻔했다. 다행히 E번이라 아무도 건드리지 않았다 휴 다행이야

 

#include <iostream>
#include <algorithm>
typedef long long ll;
using namespace std;
const int MAX = 123457;
ll N, S, ans = 1, maxHP = 1;
int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);

	cin >> N >> S;
	for (int i = 0; i < N; i++) {
		ll ch, s, hp;
		cin >> ch >> s >> hp;
		// 몬스터의 방
		if (ch == 1) {
			ans += s * ((hp - 1) / S);
			maxHP = max(maxHP, ans);
		}
		// 포션이 있는 방
		else {
			S += s;
			ans -= hp;
			if (ans <= 0)
				ans = 1;
		}
	}
	cout << maxHP << "\n";
}

 


F번은 아직 안풀었습니다 ㅎㅎ

반응형