Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

20-jung0115 #180

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

20-jung0115 #180

wants to merge 21 commits into from

Conversation

jung0115
Copy link
Member

@jung0115 jung0115 commented Oct 19, 2024

➡️ 풀이 코드

🔗 문제 링크

프로그래머스 | 백트래킹 - 불량 사용자(Lv.3)

개발팀 내에서 이벤트 개발을 담당하고 있는 "무지"는 최근 진행된 카카오이모티콘 이벤트에 비정상적인 방법으로 당첨을 시도한 응모자들을 발견하였습니다. 이런 응모자들을 따로 모아 불량 사용자라는 이름으로 목록을 만들어서 당첨 처리 시 제외하도록 이벤트 당첨자 담당자인 "프로도" 에게 전달하려고 합니다. 이 때 개인정보 보호을 위해 사용자 아이디 중 일부 문자를 '' 문자로 가려서 전달했습니다. 가리고자 하는 문자 하나에 '' 문자 하나를 사용하였고 아이디 당 최소 하나 이상의 '*' 문자를 사용하였습니다.
"무지"와 "프로도"는 불량 사용자 목록에 매핑된 응모자 아이디를 제재 아이디 라고 부르기로 하였습니다.

예를 들어, 이벤트에 응모한 전체 사용자 아이디 목록이 다음과 같다면

응모자 아이디
frodo
fradi
crodo
abc123
frodoc

다음과 같이 불량 사용자 아이디 목록이 전달된 경우,

불량 사용자
frd
abc1**

불량 사용자에 매핑되어 당첨에서 제외되어야 야 할 제재 아이디 목록은 다음과 같이 두 가지 경우가 있을 수 있습니다.

제재 아이디 1 제재 아이디 2
frodo fradi
abc123 abc123

이벤트 응모자 아이디 목록이 담긴 배열 user_id와 불량 사용자 아이디 목록이 담긴 배열 banned_id가 매개변수로 주어질 때, 당첨에서 제외되어야 할 제재 아이디 목록은 몇가지 경우의 수가 가능한 지 return 하도록 solution 함수를 완성해주세요.

✔️ 소요된 시간

1시간 20분

✨ 수도 코드

처음에는 불량 사용자 아이디 목록에 매칭되는 응모자 아이디를 각각 구한 다음 그 수를 이용해서 경우의 수를 구하려고 했습니다
예를 들어 불량 아이디에 매칭되는 응모자가 1번에 3명, 2번에 2명이라면 3 * 2 = 6으로 해서 총 6가지 경우의 수가 가능하다고 생각했는데, 각 불량 아이디에 중복해서 해당되는 응모자가 있을 수 있기 때문에 이 방법으로는 구할 수 없었습니다

예제 기준으로 불량 아이디가 frod*, fr*do일 경우, frodo는 둘 모두에 해당되기 때문에 처음 생각한 방식으로 하면 두 불량 아이디에 같은 응모자가 매칭되는 경우도 세어버리므로 정답이 아니게 됩니다

고민하다가 중복이 안 되게끔 경우의 수를 구하기 위해 Set을 이용했습니다!

우선 각 불량 아이디마다 응모자를 매칭해서 Set에 넣고,
결과로 나온 조합까지 Set에서 관리를 해서
하나의 경우의 수 안에서의 중복도 없고, 각 조합 경우의 수끼리도 중복이 없게 했습니다

1. 불량 아이디에 매칭되는 응모자인지 검사

  • 불량 아이디에서 *이거나, 같은 문자인지 자리마다 확인
fun isCollect(bannedId: String, userId: String): Boolean {
  if(bannedId.length != userId.length) return false
  
  for(i: Int in 0..bannedId.length - 1) {
    if(bannedId[i] != '*' && bannedId[i] != userId[i]) {
      return false
    }
  }
  
  return true
}

2. 불량 아이디마다 매칭되는 응모자 리스트 관리

  • 위에서 정의한 함수를 이용해서 매칭되는지 확인하고, list에 정리
collectIds = Array(banned_id.size) { mutableListOf<String>() }
    
// banned_id에 각각 올 수 있는 user_id 찾기
for(i: Int in 0..banned_id.size - 1) {
  for(j: Int in 0..user_id.size - 1) {
    if(isCollect(banned_id[i], user_id[j])) {
      collectIds[i].add(user_id[j])
    }
  }
}

3. 조합 만들기

  • dfs & 백트래킹
  • 해당 불량 아이디에 매칭되는 아이디지만, 이미 조합에 포함되어 있을 경우 패스
  • 조합에 포함시킬 수 있다면 포함시키고 재귀
  • 모든 불량 아이디에 응모자를 매칭했다면, 정답 set에 넣어서 중복 없이 관리 -> 이때 기존의 set을 복사하여 새로운 HashSet을 추가해야 재귀 과정에서 값 덮어씌워지는 문제가 안 생김
fun dfs(set: HashSet<String>, cnt: Int) {
  if(cnt == collectIds.size) {
    answer.add(HashSet(set))
    return
  }
  
  for(userId in collectIds[cnt]) {
    if(set.contains(userId)) continue
    
    set.add(userId)
    dfs(set, cnt + 1)
    set.remove(userId)
  }
}

위 단계를 거쳐서 정답을 구해낼 수 있었습니다!!

✅ 최종 코드

class Solution {
  lateinit var userIdsClone: Array<String>
  lateinit var bannedIdsClone: Array<String>
  lateinit var collectIds: Array<MutableList<String>>
  
  val answer = HashSet<HashSet<String>>()
  
  fun solution(user_id: Array<String>, banned_id: Array<String>): Int {
    userIdsClone = user_id.clone()
    bannedIdsClone = banned_id.clone()
    
    collectIds = Array(banned_id.size) { mutableListOf<String>() }
    
    // banned_id에 각각 올 수 있는 user_id 찾기
    for(i: Int in 0..banned_id.size - 1) {
      for(j: Int in 0..user_id.size - 1) {
        if(isCollect(banned_id[i], user_id[j])) {
          collectIds[i].add(user_id[j])
        }
      }
    }
    
    dfs(HashSet<String>(), 0)
    
    return answer.size
  }
  
  fun dfs(set: HashSet<String>, cnt: Int) {
    if(cnt == collectIds.size) {
      answer.add(HashSet(set))
      return
    }
    
    for(userId in collectIds[cnt]) {
      if(set.contains(userId)) continue
      
      set.add(userId)
      dfs(set, cnt + 1)
      set.remove(userId)
    }
  }
  
  fun isCollect(bannedId: String, userId: String): Boolean {
    if(bannedId.length != userId.length) return false
    
    for(i: Int in 0..bannedId.length - 1) {
      if(bannedId[i] != '*' && bannedId[i] != userId[i]) {
        return false
      }
    }
    
    return true
  }
}

📚 새롭게 알게된 내용

Copy link
Member

@janghw0126 janghw0126 left a comment

Choose a reason for hiding this comment

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

이 문제는 불량 아이디를 정규식으로 바꾸는 것과 가능한 아이디의 모든 경우를 찾는 것이 핵심 아이디어인 것 같습니다. 이 아이디어를 떠올리기까지 많은 시간이 걸렸는데 정미님의 친절한 수도코드와 다른 분들의 풀이를 참고하여 코드를 작성해나갔습니다!

import re
from itertools import permutations

def solution(user_id, banned_id):
    banned_patterns = [b.replace("*", ".") for b in banned_id]
    unique_matches = set()

    for users in permutations(user_id, len(banned_id)):
        if all(re.fullmatch(pattern, user) for pattern, user in zip(banned_patterns, users)):
            unique_matches.add(tuple(sorted(users)))

    return len(unique_matches)

저는 응모자 아이디의 모든 순열을 탐색하여 각 순열이 불량 사용자의 패턴과 길이를 정확히 만족하는지 검사하고, 조건을 만족하는 경우 중복이 없도록 결과에 추가하고, 최종적으로 고유한 조합의 개수를 반환하였습니다.

코드를 하나하나 참고하면서 따라 작성해보니까 확실히 이해하게 되었네요!
이번 PR도 수고하셨습니다😆

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.

2 participants