Skip to content

Commit

Permalink
[동적 계획법과 최단 거리 역추적] 11월 9일 -update
Browse files Browse the repository at this point in the history
  • Loading branch information
spiritstone committed Nov 16, 2021
1 parent 5c14270 commit 037a9e8
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 0 deletions.
107 changes: 107 additions & 0 deletions 11월 9일 - 동적 계획법과 최단 거리 역추적/15683_re.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// Created by user on 2021-11-16.
//

#include <iostream>
#include <vector>

using namespace std;

int n, m, ans = 65; // n: 세로 개수, m: 가로 개수
vector<vector<int>> board; // 사무실 상태 표시할 보드

//cctv 범위 표시(체크)
void ray(int row, int col, int dir) {
int dr[4] = {-1, 0, 1, 0};
int dc[4] = {0, 1, 0, -1};
// (r, c): (-1, 0): 상
// (0, 1): 우
// (1, 0): 하
// (0, -1): 좌
while (row >= 0 && row < n && col >= 0 && col < m && board[row][col] != 6) { //dir 방향으로 뻗어나가며 cctv 표시
if (board[row][col] == 0) //다른 cctv를 지우지 않기 위해 빈 공간일 때만 표시
board[row][col] = 7; // 범위를 벗어나거나 벽을 만나면 visited 표시(여기선 7)
row += dr[dir]; // row 갱신
col += dc[dir]; // col 갱신
}
}

//cctv 방향 지정
void install(int cctv, int row, int col, int dir) {
// dir == 1일 경우 예시(블로그 참고)
if (cctv >= 1) //1, 2, 3, 4, 5번 cctv
ray(row, col, dir); //
if (cctv >= 2 && cctv != 3) //2, 4, 5번 cctv
ray(row, col, (dir + 2) % 4); // 위 아래
if (cctv >= 3) //3, 4, 5번 cctv
ray(row, col, (dir + 1) % 4); // 우측 추가
if (cctv == 5) //5번 cctv
ray(row, col, (dir + 3) % 4); // 좌측 추가
}

//사각지대 계산
int blindSpot() {
int cnt = 0;
for (int i = 0; i < n; i++) { // 모든 cctv 배치
for (int j = 0; j < m; j++) {
if (!board[i][j]) // board[i][j] == 0인 경우
cnt++; // cnt 증가
}
}
return cnt; // 사각지대 리턴
}

void backtracking(int idx) { // 백트래킹 함수
if (idx == n * m) { //기저 조건 : 사무실의 모든 위치 확인
ans = min(ans, blindSpot()); //사각지대 계산 후 최솟값 갱신
return; // 함수 종료
}
// idx != n * m
int row = idx / m; // row값 초기화
int col = idx % m; // col값 초기화
int cur = board[row][col]; // 이번에 설치할 cctv
if (cur == 0 || cur == 6 || cur == 7) //cctv가 없는 곳
return backtracking(idx + 1); // 재귀 호출

vector<vector<int>> save = board; //unvisited 처리용 board 상태 저장
for (int i = 0; i < 4; i++) { //4개의 방향에 대해 cctv 설치
install(cur, row, col, i); // visited 처리
backtracking(idx + 1); // 재귀 호출
board = save; // unvisited 처리

//2번 cctv의 방향 종류는 2개, 5번 cctv의 방향 종류는 1개
if ((cur == 2 && i == 1) || (cur == 5 && i == 0))
break; // 체크 후 backtracking 종료
}
}

/**
* cctv가 설치될 수 있는 모든 경우를 고려하는 완전탐색 문제
*
* 1. 각 cctv에 대해 가능한 모든 방향을 고려하여 설치
* 1, 3, 4번 cctv : 4방향
* 2번 cctv : 2방향
* 5번 cctv : 1방향
* 2. install 함수에서 각 cctv의 빛이 뻗어나갈 방향을 잡음
* 3. ray 함수에서 cctv의 감시 가능 범위 표시
* 4. 모든 위치를 확인했으면 blindSpot 함수를 통해 사각지대 계산
*
* 풀이 : https://myunji.tistory.com/411
* 해당 코드는 위 링크의 코드를 리팩토링한 코드입니다.
* 말로는 풀이를 설명하기 어려우니 링크를 꼭 확인해주세요!
*/
int main() {
//입력
cin >> n >> m; // 사무실의 세로와 가로 입력받기
board.assign(n, vector<int>(m));
for (int i = 0; i < n; i++) { // 세로 만큼
for (int j = 0; j < m; j++) // 가로 만큼
cin >> board[i][j]; // 보드 값 입력
}

//연산
backtracking(0);

//출력
cout << ans;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Created by user on 2021-11-16.
//

#include <iostream>
#include <vector>

using namespace std;
const int INF = 1e5 * 2; //최대 n-1개의 간선을 지나게 됨

void floydWarshall(int n, vector<vector<int>> &graph, vector<vector<int>> &table) { // 간선 정보와 경로 정보 순서대로 입력받음
for (int k = 1; k <= n; k++) { // 집하장 n개
for (int i = 1; i <= n; i++) { // 집하장 n개
for (int j = 1; j <= n; j++) { // 집하장 n개
int new_dist = graph[i][k] + graph[k][j]; //중간에 k를 거쳐서 i에서 j로 감
if (new_dist < graph[i][j]) { //i->k->j가 i->j보다 빠른 경로라면
graph[i][j] = new_dist; // i->j의 간선정보 갱신
table[i][j] = table[i][k]; // i->j의 경로를 i->k의 중간 경로(table[i][k])로 갱신
}
}
}
}
}

/**
* graph : 플로이드-워셜 결과 행렬 그래프
* table : 경로표. table[i][j] = i->j로 가기 위해 제일 먼저 거쳐야 하는 정점
*
* 1. i->j의 중간 경로를 i로 초기화
* 2. i->k->j가 i->j보다 짧다면 i->j의 중간 경로를 i->k의 중간 경로(table[i][k])로 갱신
* k로 갱신하는게 아니라 table[i][k]로 갱신하는 이유는?
* 만약 i->k의 경로가 i->t->k라면 최종 경로는 i->t->k->j
* 바로 k로 갱신하면 t를 놓칠 수 있기 때문에 table[i][k]로 갱신
* line 16을 table[i][j] = k로 바꾸면 결과가 어떻게 되는지 확인해보세요
*/
int main() {
int n, m, s, d, c; // n: 집하장 개수, m: 집하장간 경로 개수, s:start 집하장 번호, d: destination 집하장 번호, c: 오가는데 필요한 시간

//입력
cin >> n >> m;
vector<vector<int>> graph(n+1, vector<int>(n+1, INF)); // 간선정보 저장
vector<vector<int>> table(n+1, vector<int>(n+1, 0)); // 경로 정보 저장
for (int i = 1; i <= n; i++)
graph[i][i] = 0; // 집하장 i에서 i로 가는 데 걸리는 시간은 0

while (m--) { //무방향 그래프
cin >> s >> d >> c; // 모든 경로의 집하장과 소요시간 입력
//간선 정보
graph[s][d] = graph[d][s] = c; // 집하장 s와 d를 오가는데 필요한 시간은 c

//경로 정보 (1. table[i][j]에서 i->j의 중간 경로를 i로 초기화 => 제일 먼저 거쳐야 하는 정점은 j)
table[s][d] = d; // s -> d로 가기 위해 제일 먼저 거쳐야 하는 정점: d
table[d][s] = s; // d -> s로 가기 위해 제일 먼저 거쳐야 하는 정점: s
}

//연산
floydWarshall(n, graph, table);

//출력
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) { // 집하장 n개 -> table 크기: n X n
if (i == j) // table[i][i]에는
cout << "- "; // '-' 출력
else // 그 외의 경우에는
cout << table[i][j] << ' '; // 경로 정보 출력
}
cout << '\n'; // 구분선 출력
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// Created by user on 2021-11-16.
//

#include <iostream>
#include <vector>

using namespace std;
const int SIZE = 19;

//범위와 돌의 종류가 유효한지 확인
bool promising(int r, int c, int stone, vector<vector<int>> &board) {
return r >= 0 && r < SIZE && c >= 0 && c < SIZE && board[r][c] == stone;
// r과 c가 모두 오목판의 범위 내에 있고, 보드 값이 stone 값과 같을 때만 true 반환
}
// 육목 아니고, 확실히 이겼는지 확인하는 함수
bool validDir(int r, int c, int d, int stone, vector<vector<int>> &board) {
// 구해야 하는 돌은 가장 왼쪽 => 가로, 세로, 우하향 대각선, 우상향 대각선
int dr[4] = {0, 1, 1, -1}; // row 값
int dc[4] = {1, 0, 1, 1}; // col 값

//(r, c) 이전에 위치한 이어지는 돌이 있나? => (r, c) 왼쪽 방향
bool is_six = promising(r - dr[d], c - dc[d], stone, board);

int cnt = 0; // 돌이 연속하는 횟수
while (cnt < 6 && promising(r, c, stone, board)) { //(r, c)를 가장 왼쪽으로 하는 이어지는 바둑알의 개수
cnt++; // 연속 횟수 추가
r += dr[d]; // r값 갱신
c += dc[d]; // c값 갱신
}
return cnt == 5 && !is_six; // 오목이고, 육목이 아닐 경우 true 반환
}
// 이겼는지 판단하는 함수
bool isEnd(int r, int c, vector<vector<int>> &board) {
for (int i = 0; i < 4; i++) { //가로, 세로, 우하향 대각선, 우상향 대각선
if (validDir(r, c, i, board[r][c], board)) // true반환하면 이긴거임
return true; // 이겼으니 true
}
return false; // 이기지 않음
}

/**
* 1. 특정 좌표(r, c)를 가장 왼쪽으로 하는 가능한 모든 오목 배치에 대해 오목 여부 확인
* 가능한 모든 배치 : 오른쪽, 아래, 우하향, 우상향
* 2. 육목이상이 되는 경우 : (r, c) 왼쪽에 같은 돌이 있는 경우
*/
int main() {
// 보드판 생성
vector<vector<int>> board(SIZE, vector<int>(SIZE, 0));

//입력
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++)
// 오목판 입력. 검: 1, 흰: 2, 빈: 0
cin >> board[i][j];
}

//연산 & 출력
for (int i = 0; i < SIZE; i++) { // 오목판크기만큼 돌면서
for (int j = 0; j < SIZE; j++) {
if (!board[i][j]) //돌이 없음 (board값 == 0일때)
continue;
if (isEnd(i, j, board)) { //누군가 이겼나?
cout << board[i][j] << '\n' << i + 1 << ' ' << j + 1;
// 이겼다면 돌의 색 출력하고 i와 j가 0부터 시작하니까 둘에다가 1을 더한 값을 출력한다.
return 0;
}
}
}
cout << 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Created by user on 2021-11-16.
//

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int n, m;
//상, 좌, 좌상향
int dr[3] = {-1, 0, -1};
int dc[3] = {0, -1, -1};

//역추적
string back(string str1, vector<vector<int>> &path) {
string result = ""; // 결과 저장할 변수
int r = n, c = m; //
while (path[r][c] != -1) {
int d = path[r][c];
if (d == 2) //좌상향에서 가져온 경우 -> str1[r - 1] == str2[c - 1]
result += str1[r - 1]; //
r += dr[d]; //역추적
c += dc[d]; //역추적
}
reverse(result.begin(), result.end()); // 공통 문자열 다시 순서대로 정렬
return result; // 공통 문자열 리턴
}

//LCS 길이 구하는 함수
int lcs(string str1, string str2, vector<vector<int>> &path) {
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0)); // 두 문자열의 길이만큼의 2차원 벡터 생성
for (int i = 1; i <= n; i++) { // 첫번째 문자열 길이만큼 반복
for (int j = 1; j <= m; j++) { // 두번째 문자열 길이만큼 반복
if (str1[i - 1] != str2[j - 1]) { //두 문자가 서로 다르면
dp[i][j] = dp[i - 1][j]; //우선 위쪽 값 가져온 것으로 저장
path[i][j] = 0; //경로(방향) 저장
if (dp[i][j] < dp[i][j - 1]) { //왼쪽이 더 크다면
dp[i][j] = dp[i][j - 1]; // 왼쪽 값으로 저장
path[i][j] = 1; //경로(방향) 저장
}
} else if (str1[i - 1] == str2[j - 1]) { //두 문자가 서로 같다면
dp[i][j] = dp[i - 1][j - 1] + 1; // 좌측 상향 대각선보다 1 큰 값 저장(해당 문자들이 포함되기 전에 길이 + 1)
path[i][j] = 2; //경로(방향) 저장
}
}
}
return dp[n][m]; //LCS 길이 리턴
}

/**
* 기본 문제: LCS (해당 풀이는 "08. 동적계획법.pdf" 참고)
*
* [역추적]
* - 위쪽, 왼쪽, 좌상향 중 어느 방향에서 왔는지 경로를 저장한 후, 역추적하는 문제
* - 경로 저장은 dp배열이 갱신될 때 함
*
* 해당 풀이는 인덱스를 편하게 관리하기 위해 dp와 path 배열을 (1, 1)부터 시작
*/

int main() {
string str1, str2;

//입력
cin >> str1 >> str2; // 두 문자열 입력
n = str1.length(); // 첫번째 문자열의 길이
m = str2.length(); // 두번째 문자열의 길이
vector<vector<int>> path(n + 1, vector<int>(m + 1, -1)); //그 전 방향 저장

//연산
int ans = lcs(str1, str2, path); //lcs
string result = back(str1, path); //역추적

//출력
cout << ans << '\n' << result << '\n';
return 0;
}

0 comments on commit 037a9e8

Please sign in to comment.