Skip to content

Conversation

@Mingguriguri
Copy link
Collaborator

@Mingguriguri Mingguriguri commented Apr 19, 2025

🌱WIL

이번 한 주의 소감을 작성해주세요!

  • 이번 주에는 백트래킹(그래프) 문제 2개, DP 문제 2개를 풀었다.
  • 백트래킹 문제를 너무 오랜만에 풀어서 구조를 잡는 것부터 난관이었지만 복습할 수 있어 좋았다. 또 다른 풀이보다 특히 백트래킹 유형은 반드시 백트래킹으로만 풀이하지 않고 다양한 풀이로 접근해도 풀리는 문제가 많아 다양한 풀이는 보는 재미가 있었다.
  • DP 문제 중 동전 문제는 비슷한 문제를 과거에 풀이했음에도 2시간가량 붙잡아서 좀 현타가 왔다. 처음에 2차원 DP로 접근했고, 최대 범위인 10000 x 10000 배열을 미리 만들어서 접근하려고 했다. 하지만 메모리 낭비가 심하고 인덱싱 규칙이 복잡해 실패했다... 이런 경우는 테스트케이스마다 동전 종류가 다르므로, 미리 큰 범위를 계산해놓는 방식은 비효율적이라는 것을 알게 되었다.
  • DP 문제 중 팰린드롬 공장 문제는 너~무 어려워서 아직도 잘 이해가 안 간다.. DP는 대체 어디까지 어려울 수 있을까? 편집거리 알고리즘이라 생각했는데 이를 이 문제의 '팰린드롬 수' 개념에 맞춰 변형해야 하는 게 너무 헷갈리고 어려웠다.

🚀주간 목표 문제 수: 3개

푼 문제


백준 #1759. 암호만들기: 그래프 / 골드1

정리한 링크: (바로가기)

🚩플로우 (선택)

코드를 풀이할 때 적었던 플로우가 있나요?

  1. 입력 처리
    • 암호 길이 L, 전체 알파벳 개수 C를 입력받고
    • 알파벳 리스트를 사전 순으로 정렬한다.
  2. 유효성 검사 함수 is_valid 정의
    • 모음이 최소 1개 이상, 자음이 최소 2개 이상인지 확인한다.
  3. 백트래킹 함수 backtrack 정의
    • 현재 암호 길이가 L이면 종료
    • 유효성 검사 후 조건에 맞으면 출력
    • 반복문을 통해 알파벳을 추가하고, 재귀 호출로 조합 생성
  4. 초기 호출
    • backtrack([], 0)으로 시작한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

L, C = map(int, input().split())  # L: 암호 길이, C: 문자 종류
chars = sorted(input().split())   # 사전순 정렬
vowels = {'a', 'e', 'i', 'o', 'u'}


def is_valid(word):
    # 최소 한 개의 모음과 최소 두 개의 자음으로 구성되어있는지 확인
    vowel_cnt, consonant_cnt = 0, 0  # 모음 개수, 자음 개수
    for w in word:
        if w in vowels:
            vowel_cnt += 1
        else:
            consonant_cnt += 1

    return vowel_cnt >= 1 and consonant_cnt >= 2


def backtrack(word, start):
    if len(word) == L:  # 종료 조건
        if is_valid(word):
            print(''.join(word))
        return

    for i in range(start, C):
        word.append(chars[i])
        backtrack(word, i+1)
        word.pop()

backtrack([], 0)

💡TIL

배운 점이 있다면 입력해주세요

  • 백트래킹 문제를 오랜만에 풀다 보니 구조를 잡는 것부터 고민이 되었다.
  • 처음에는 조합을 다 구한 뒤 조건을 필터링하려 했지만, 백트래킹 방식으로 조건을 탐색 중에 필터링하는 쪽이 더 효율적이고 유연하다는 것을 체감했다.
  • 다양한 풀이 방식을 비교하면서 문제 해결 방법에 정답은 없고, 목적과 맥락에 따라 적절한 접근법을 선택하는 게 중요하다는 걸 느꼈다.
  • 이번 문제를 통해 백트래킹, 조합, 문자열 조건 판별 등 코딩 테스트에서 자주 나오는 패턴을 복습할 수 있었다.

백준 #6603. 로또: 그래프 / 실버2

정리한 링크: (바로가기)

🚩플로우 (선택)

코드를 풀이할 때 적었던 플로우가 있나요?

조합 풀이:

  • 테스트케이스를 반복 입력받는다.
    • 0이 입력되면 반복 종료
    • k와 집합 S를 분리하고한다.
  • combinations(S, 6)으로 가능한 조합을 모두 구한다.
  • 조합을 한 줄씩 출력하고, 테스트케이스 사이에 빈 줄을 출력한다.

백트래킹 풀이:

  1. 테스트케이스를 입력받고, 종료 조건(0)이 입력되면 종료한다.
  2. k와 집합 S를 분리한다.
  3. 백트래킹 함수 backtrack(lotto, current)를 정의한다.
    • lotto는 현재까지 고른 수
    • current는 탐색을 시작할 인덱스
  4. 길이가 6이 되면 출력하고 종료한다.
  5. 반복문으로 다음 숫자를 선택하고 재귀 호출한다.
  6. 백트래킹으로 마지막 원소를 제거하고 돌아간다.

🚩제출한 코드

조합 풀이:

import sys
from itertools import combinations
input = sys.stdin.readline

"""
#6603. 로또
조합 풀이
"""

while True:
    testcase = input().strip()
    if testcase == '0':
        break
    nums = list(map(int, testcase.split()))
    k, S = nums[0], nums[1:]
    combs = list(combinations(S, 6))
    for comb in combs:
        print(' '.join(map(str, comb)))
    print()

백트래킹 풀이:

import sys
input = sys.stdin.readline

"""
#6603. 로또
백트래킹 풀이
"""

def backtrack(lotto, current):
    if len(lotto) == 6:  # 종료조건
        print(' '.join(map(str, lotto)))
        return

    for i in range(current, k):
        lotto.append(S[i])
        backtrack(lotto, i+1)
        lotto.pop()


while True:
    testcase = input().strip()
    if testcase == '0':
        break
    nums = list(map(int, testcase.split()))
    k, S = nums[0], nums[1:]

    backtrack([], 0)
    print()

💡TIL

배운 점이 있다면 입력해주세요

  • 지난 스터디에서 발제 문제로 풀었던 [백준] #1759. 암호 만들기 (파이썬/Python) 문제에 이어서 풀어서 그런지, 백트래킹 구조를 생각해내고 파악하는 것을 손쉽게 할 수 있었다.
  • 조합 문제는 itertools로 빠르게 풀 수 있지만, 조건이 추가되거나 상황이 복잡해지면 백트래킹이 유연하게 대응할 수 있다.

백준 #9084. 동전: DP / 골드5

정리한 링크: (바로가기)

🚩플로우 (선택)

코드를 풀이할 때 적었던 플로우가 있나요?

  1. 테스트케이스 개수 T를 입력받는다.
  2. T번 반복하면서:
    • 동전의 수 N 입력
    • 오름차순 동전 리스트 coins 입력
    • 만들 금액 M 입력
  3. dp[0] = 1로 초기화된 dp 배열을 생성
  4. 동전 하나씩 순회하며 dp 갱신
  5. dp[M]을 출력한다.

🚩제출한 코드

import sys
input = sys.stdin.readline

T = int(input())
for _ in range(T):
    N = int(input())  # 동전의 가지 수
    coins = list(map(int, input().split()))  # N가지 동전의 각 금액
    M = int(input())  # 목표 금액

    dp = [0] * (M+1)
    dp[0] = 1

    for coin in coins:
        for i in range(coin, M + 1):
            dp[i] += dp[i - coin]

    print(dp[M])

💡TIL

배운 점이 있다면 입력해주세요

  • 규칙을 찾는 게 쉬우면서도 어렵다. 거의 다 찾았다고 생각했을 때에는 그 다음 난관이 있었다. 너무 복잡하게 생각하는 것도 안 좋은 것 같다.
  • 처음엔 어렵지 않다고 생각했지만, DP 배열을 어떻게 정의할지에서 오랜 시간 막혔다.
  • 비슷한 문제인 동전 1번 문제에서 사용했던 방식으로 접근했더니 해결이 됐다.
  • 테스트케이스마다 동전 종류가 다르므로, 미리 큰 범위를 계산해놓는 방식은 비효율적이었다.

백준 #1053. 팰린드롬 공장: DP / 골드1

정리한 링크: (바로가기)

🚩플로우 (선택)

코드를 풀이할 때 적었던 플로우가 있나요?

  1. dp_ops(s) 함수
    • 문자열 s를 받아서 dp 테이블을 채우고 dp[0][n-1] 반환
  2. 메인 로직
    • 원본 문자열에 대해 ans = dp_ops(s)
    • ans > 1 일 때만(0이나 1이면 스왑해도 더 줄일 수 없음)
      • 모든 i<j 쌍에 대해
        1. s[i]s[j]를 바꾼 새 문자열 만들기
        2. cost = 1 + dp_ops(swapped_string) 계산
        3. cost < ans 이면 ans = cost
    • 최종 ans 출력

🚩제출한 코드

import sys
input = sys.stdin.readline


def min_ops_to_pal(s):
    """
    s: list of chars
    return: 최소 편집 연산(삽입, 삭제, 교체만 허용)으로 s를 팰린드롬으로 만드는 비용
    """
    n = len(s)
    # dp[i][j]: s[i..j]를 팰린드롬으로 만드는 최소 연산 횟수
    dp = [[0] * n for _ in range(n)]
    # 길이 2부터 n까지 늘려가며
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            if s[i] == s[j]:
                dp[i][j] = dp[i + 1][j - 1]
            else:
                # 교체(대칭 맞추기), 삭제(왼쪽), 삭제(오른쪽)
                dp[i][j] = min(
                    dp[i + 1][j - 1] + 1,
                    dp[i + 1][j] + 1,
                    dp[i][j - 1] + 1
                )
    for d in dp:
        print(d)

    return dp[0][n - 1] if n > 0 else 0


s = list(input().rstrip())
n = len(s)

# 1. 교환 없이 삽입/삭제/교체만 사용했을 때
ans = min_ops_to_pal(s)
if ans <= 1:
    print(ans)
    exit()

# 2. 서로 다른 문자끼리 한 번만 교환을 허용해봤을 때, 교환 비용 1을 더해보고 개선되는지 확인
for i in range(n):
    for j in range(i + 1, n):
        if s[i] != s[j]:
            # 문자가 같지 않은 경우, SWAP
            s[i], s[j] = s[j], s[i]
            swap_cost = min_ops_to_pal(s) + 1  # swap 비용 포함
            if swap_cost < ans: # swap한 경우가 더 작다면 ans에 업데이트
                ans = swap_cost
            # 복구
            s[i], s[j] = s[j], s[i]
print(ans)

💡TIL

배운 점이 있다면 입력해주세요

Copy link
Member

@YoonYn9915 YoonYn9915 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 이번 주 발제, 과제 문제를 풀며 민정님과 비슷한 생각을 했네요.
'다양한 풀이 방식을 비교하면서 문제 해결 방법에 정답은 없고, 목적과 맥락에 따라 적절한 접근법을 선택하는 게 중요하다는 걸 느꼈다.'

로또 문제는 combinations와 backtracking 등 여러 방법으로 풀 수 있지만 조건이 여러개 붙는다는 가정을 해보면 backtracking으로 푸는게 정석인거 같아요.
한주 수고하셨습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

발제 당시 백트레킹으로 풀 수 있는 건 알았지만 어떻게 구현해야 할지 감이 안 잡혔는데 제공해주신 백트레킹 템플릿과 발제 자료를 통해 확실하게 이해한 것 같습니다. 이번 과제는 담 주에 이어서 풀어오도록 하겠습니다.
좋은 문제 공유해 주셔서 감사합니다.

@Mingguriguri Mingguriguri merged commit 543215d into main Apr 20, 2025
@github-actions
Copy link

🔥2025-04 챌린지 진행 상황

👉 그래프

  • YoonYn9915: 0개 ❌
  • Mingguriguri: 2개 ❌
  • zaqquum: 4개 ❌

👉 DP

  • YoonYn9915: 2개 ❌
  • Mingguriguri: 6개 ✅
  • zaqquum: 4개 ❌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants