[프로그래머스] lv1 풀이

Sdoubleu·2026년 9월 8일

프로그래머스

목록 보기
36/36
post-thumbnail

신규 아이디 추천

  • 난이도: lv 1 / 걸린 시간: 60분

  • 내 첫 접근: 주어진 7단계를 이용하기

  • 막혔던 지점:
    1~3단계는 순차적으로 해놓고, 4~7단계는 한 번에 처리하려했기에 코드가 꼬였다

  • 내 코드

class Solution {
    fun solution(new_id: String): String {
        val regex = "[^a-z0-9\\-_.]".toRegex()
        val dot = ".."
        var answer: String = new_id.lowercase().replace(regex, "")
        while (true) {
            if (dot in answer) answer = answer.replace(dot, ".")
            else break
        }
        
        if (answer.first() == '.') answer = answer.substring(1 .. answer.lastIndex)
        if (answer.isNotEmpty() && answer.last() == '.') answer = answer.substring(0 until answer.lastIndex)
    
        if (answer.isEmpty()) answer = "a"

        if (answer.length > 15) {
            answer = answer.substring(0 until 15)
            if (answer.last() == '.') answer = answer.substring(0, answer.lastIndex)
        } else if (answer.length < 3) {
            while (answer.length < 3) {
                answer += answer.last()
            }
        }
    
        return answer
    }
}

으으

  • 지문에 이미 순서와 조건이 명확히 적혀있었는데, 코드 구조가 그 순서를 그대로 안 따라갔고,
    "단계가 여러 개 있는 문제"를 만나면, 각 단계를 지문 순서 그대로 하나씩 독립적으로 실행되게 짜는 습관을 가지기

  • 해당 문제에 처리 단계인 4단계를 보면 "new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다." 인데, 여기에서
    무의식적으로 if - else if 를 이용해서 한 가지만 처리해서 넘겨버린 것이 문제였다.
    (ex: .age. 가 테스트 케이스로 들어왔다면, age.로 됐을 것이다 .
    "두 조건이 동시에 참일 수 있는가?"를 항상 먼저 의심하는 습관을 갖도록 하자

  • substring의 "빈 범위" 성질을 우연히 발견 .. 너무 늦게 발견
    substring(1..0)처럼 범위 자체가 거꾸로(시작>끝)면, 코틀린이 알아서 빈 문자열로 처리해준 것을 기억하자


유연 근무제

  • 난이도: lv 1 / 걸린 시간: 60분

  • 내 첫 접근: startday부터 늘려가면서 토요일, 일요일을 걸러내고 시각 비교하기

  • 막혔던 지점: 855(8시 55분)처럼 분이 50~59 사이일 때 10분을 더하면 시가 넘어가는 부분에서 생각을 못하고 +10 만 해버림
    생각 후 문자열리스트로 바꿔서 해볼까도 했는데 아닌거같고..HHMM 형식이라 /100 , %100으로 분리 -> 각각 연산 -> 다시 합치기

  • 실제 사용한 자료구조/패턴: 특별한 자료구조는 없어도 됐고, 시/분을 분리해서 계산하는 산술 처리 + 요일 순환 카운터 조합으로 풀리는 문제

  • 내 코드
class Solution {
    fun solution(schedules: IntArray, timelogs: Array<IntArray>, startday: Int): Int {
        var answer: Int = 0
        
        for(i in 0 until schedules.size) {
            var hour = schedules[i] / 100
            var minute = schedules[i] % 100 + 10
            if(minute >= 60) {
                hour += 1
                minute -= 60
            }
            val time = hour * 100 + minute
            
            var dayCnt = startday
            var cnt = 0
            for(j in timelogs[i]) {
                if(dayCnt % 8 == 6) {
                    dayCnt++
                    continue
                } else if(dayCnt % 8 == 7) {
                    dayCnt = 1
                    continue
                }
                
                dayCnt++
                if(j > time) cnt++
                
            }
            if(cnt == 0) answer++
        }
        
        return answer
    }
}

으으

  • 숫자 하나에 여러 단위(시/분)가 압축되어 있는 갑을 만나면, 문자열/리스트로 쪼개서 접근하기 보다 /, % 연산으로 단위별로 분리하는게 더 직관적이다는 것을 배움
  • 일단 더하고, 넘치면 그 때 자리올림 이라는 순서가
    "미리 넘칠지 안 넘칠지 조건부터 따지는 방식"보다 코드가 짧고 실수가 적다는 것을 확인함
if(minute+10>60) {...} else {...}

처럼 나눠서 쓰다가 실수했던 것 

minute = 원래 분 + 10 먼저 계산해두고 넘침만 체크

신고 결과 받기

  • 난이도: lv 1 / 걸린 시간: 60분

  • 문제 한 줄 요약:
    유저들이 서로를 신고하는데, k번 이상 신고당한 사람은 "정지" 되고, 그 사람을 신고했던 사람들 전원에게 메일이 1통씩 간다. 각 유저가 받는 메일 개수를 구하는 문제.

  • 내 첫 접근:

    • 누가 누굴 신고했는지, 그 사람이 몇 번 신고 당했는지 둘 다 필요하다는 것까지는 파악
    • 처음엔 report 전체를 1000x1000 2차원 배열로 관리하려다가, 크기 부담을 느끼고 다른 방법을 고민함
  • 실제 사용한 자료구조/패턴:
    자료구조: Map<String, MutableSet<String>> (신고자 -> 신고 대상 목록) +
    Map<String, Int> (유저 -> 신고당한 횟수

패턴:
1. 방향성 있는 관계는 "누가 누구를"의 방향을 명확히 구분해서 저장 - 트리거 신호: "A가 B를 ~했다"처럼 관계에 방향이 있는 문제 -> Map<신고자, Set<대상>>처럼 key를 누구로 삼을지부터 정하기
2. "같은 원소를 여러 번 넣어도 하나로 취급"이 필요하면 Set
3. 키가 없을 수도 있는 Map에 원소를 추가할 땐 getOrPut - getOrDefault는 "없으면 기본값을 반환만" 하고 Map에 안 남기지만, getOrPut은 "없으면 실제로 만들어서 저장까지" 해줌
4. 두 집합의 관계를 확인할 땐, 집계 순서가 결과를 좌우한다.

  • 막혔던 지점:
    1차 실수: contains()의 방향을 반대로 씀
if (reportedBy.contains(j)) // j는 범인 후보

reportedBy는 신고자 -> 그 사람이 신고한 대상들인데,
contains(J)는 "j가 key로 존재하는가", 즉 이 범인이 누군가를 신고한 적이 있는가 를 묻는 거라 엉뚱한 질문이였음
-> 필요했던 건 "지금 보고 있는 신고자(name)가 이 범인(j)을 신고했는가?"

if( j in reportedBy.getOrDefault(name, mutableSetOf()) )

"A가 B를 신고했다"는 관계를 다룰 때, 지금 확인하려는 게 "A 기준으로 B를 찾는 건지, B 기준으로 A를 찾는건지" 코드 짜기 전에 말로 먼저 정리하고 들어가야 함.

2차 실수: "몇 번 신고당했는지" 어디서 세지?
ex) ["con", "ryan"], report = ["ryan con", "ryan con", "ryan con", "ryan con"] (같은 사람이 같은 사람을 4번 신고), k = 3

  • 틀렸던 방식 - report 원본을 훑으면서 바로 카운트
for (i in report) {
    val people = i.split(" ")
    reportedBy.getOrPut(people[0]) { mutableSetOf() }.add(people[1])
    map[people[1]] = map.getOrDefault(people[1], 0) + 1   // <- 바로 여기서 카운트
}

report가 4줄이니까 이 줄이 4번 실행돼서 map["con"] = 4가 됨

정답 - reportedBy를 완전히 다 채운 뒤, report 루프 밖에서 딱 한 번만 카운트:

// 1단계: report 다 훑으면서 reportedBy만 채움
for (i in report) {
    val people = i.split(" ")
    reportedBy.getOrPut(people[0]) { mutableSetOf() }.add(people[1])
}

// 2단계: report 루프가 완전히 끝난 뒤, 여기서 딱 한 번만
for ((user, suspects) in reportedBy) {
    for (s in suspects) {
        map[s] = map.getOrDefault(s, 0) + 1
    }
}

왜 이 구조가 맞는지 (메커니즘 이해)

핵심은 "바깥 루프를 무엇 기준으로 도느냐"임.

report 원본 줄 기준으로 돌면: 같은 사람이 같은 사람을 여러 번 신고한 게 줄 개수만큼 그대로 다 세짐 (중복 제거 효과 없음)
reportedBy의 "신고자 한 명씩" 기준으로 돌면: 같은 신고자의 Set엔 같은 대상이 한 번만 들어있으니, 그 신고자 차례에서 그 대상은 딱 1번만 카운트됨

즉 이 구조가 자동으로 구분해주는 것:
같은 사람의 중복 신고 → Set 덕분에 자동으로 1번
서로 다른 사람들의 각자 신고 → 바깥 루프가 사람 수만큼 도니까 자동으로 다 셈


다음에 비슷한 문제 보면 떠올릴 키워드

  • "A가 B에게 ~했다" 는 방향성 있는 관계 → Map<A, Set<B>> 형태로, 그리고
    "A 기준으로 찾을지 B 기준으로 찾을지" 먼저 말로 정리

  • "집계(카운트)는 원본 로그가 아니라, 이미 중복 제거된 구조를 기준으로" — 원본 데이터를 그대로 세면 중복이 다시 살아날 수 있음. 중복 제거가 필요한 카운트는 항상 "이미 걸러진 자료구조를 순회하면서" 세야 함

  • 중첩 루프가 있을 때, 안쪽 로직이 "몇 번 반복되어야 정확한지" 손으로 먼저 계산해보기 — 바깥 루프 위치를 report 원본으로 할지, 정제된 Map으로 할지에 따라 결과가 완전히 달라짐을 이번에 직접 경험함


profile
Android

0개의 댓글