1.언어 사용에 대한 고민

나는 그동안 python으로 알고리즘 공부를 해왔기 때문에, c++과 java만 가능한 수업이 다소 벅찼다. 그런데 대부분 c++로 알고리즘 공부를 한다고 하기도 해서, 이번주까지는 파이썬과 병행하고 다음주부터는 c++로 공부를 이어가보려고 한다.

1644 소수의 연속합

[문제정리]

-연속된 소수의 합으로 나타낼 수 있는 경우의 수를 구해보자.

-단, 같은 소수가 중복되어 사용되면 안된다.

 

[문제 푸는 방법] 

아래의 두 가지 알고리즘을 적용해야한다.

1.에라토스테네스의 체 : 주어진 N까지의 소수를 미리 구한다.

2.투 포인터를 활용해, 연속된 소수의 합이 N이 되는지 확인하여 정답 구하기.

 

[1.에라토스테네스의체 코드 부분]

def sieve_of_eratosthenes(n):
  is_prime=[True]*(n+1)
  p=2
  while(p*p<=n):
    if is_prime[p]:
        for i in range(p*p,n+1,p):
            is_prime[i]=False
    p+=1
  return [p for p in range(2,n+1) if is_prime[p]]

n=int(input())
print(sieve_of_eratosthenes(n))

 

[2.투포인터 코드 부분]

result=0
start=0
end=0

while end<=len(prime_num):
  temp=sum(prime_num[start:end])
  if temp==n: #같으면 result 하나 카운트하고, end를 뒤로 한 칸 옮긴다.
    result+=1
    end+=1
  elif temp<n: #합이 수 보다 작으면, end를 뒤로 한 칸 옮긴다.
    end+=1
  else: #합이 수보다 커지면, start를 뒤로 한 칸 옮긴다.
    start+=1​

 

[정답코드]

def sieve_of_eratosthenes(n):
  is_prime=[True]*(n+1)
  p=2
  while(p*p<=n):
    if is_prime[p]:
      for i in range(p*p,n+1,p):
        is_prime[i]=False
    p+=1
  return [p for p in range(2,n+1) if is_prime[p]]

n=int(input())
prime_num=sieve_of_eratosthenes(n)


result=start=end=0

while(end<=len(prime_num)):
  temp=sum(prime_num[start:end])
  if temp==n:
    result+=1
    end+=1
  elif temp<n:
    end+=1
  else: #temp>n:
    start+=1

print(result)

 

2960 에라토스테네스의 체

[문제]

에라토스네테스의체는, N보다 작거나 같은 모든 소수를 찾는 유명한 알고리즘이다.

1)2부터 N까지 모든 수를 적는다

2)지우지 않은 수 중 가장 작은 수를 선택하고, 이것을 P라 한다.

3)P를 지우고, 아직 지우지 않은 P의 배수를 크기 순서대로 지운다.

4)다 완료될 때까지 2번으로 돌아간다.

첫째 줄에 N K 형태로 주어졌을 때, N보다 작거나 같은 소수들 중 K번째 지우는 수를 구하는 프로그램을 작성하시오.

 

def sieve_of_eratosthenes(n):
  delete_nums=[]
  is_prime=[True]*(n+1)
  p=2
  while(p*p<=n):
    if is_prime[p]:
      delete_nums.append(p)
      for i in range(p*p,n+1,p):
        is_prime[i]=False
        #지웠던 것을 또 지우는 과정이 발생한다. 따라서 중복되는 것은 추가하지 않게 하는 과정이 필요했다.
        if i not in delete_nums:
          delete_nums.append(i)
    p+=1
  
  #기존 코드는 전체를 우선 True로 만들어놓고, False 처리를 하여 자동으로 남겨지는 소수들이 카운트가 안됐다.
  #그냥 남겨지는 애들도 추가하기 위해서 다음 코드를 넣었다. 
  for i in range(int(n**0.5)+1,len(is_prime)):
    if is_prime[i]:
      delete_nums.append(i)
  return delete_nums

n,k=map(int,input().split())
delete_nums=sieve_of_eratosthenes(n)
print(delete_nums[k-1])
2725 보이는 점의 개수

[문제]

1.(0,0)에서 보이는 (x,y)의 개수를 구하려고 한다.(x,y>=0, 정수)

2.(0,0)에서 (x,y)가 보인다는 것은 (0,0)과 (x,y)를 연결하는 직선이 다른 점을 통과하지 않아야한다.

3.N이 주어졌을 때, 0<=x,y<=N 즉 (N,N)좌표평면에서 보이는 (x,y)좌표의 개수를 출력하시오.

4.단, (0,0)은 계산하지 않는다.

5.첫째 줄에 테스트케이스 개수C(1이상 1000이하)가 주어진다. 각 테스트케이스는 N(1이상 1000이하)로 주어지고, 한 줄에 하나씩 주어진다.

 

[시간초과코드]

def no_see_cnt(n):
  slope=[]
  #(0,0)~(n,n) 모든 경우의 수에 대하여
  for i in range(n+1):#x좌표
    for j in range(0,i+1):#y좌표
      print(f'{i},{j}')
      if i==0 and j==0:
        slope.append(0)
        print('slope에 0추가')
        break
      else:
        temp=j/i
        print(f'temp={temp}')
        if temp not in slope:
          print(f'temp={temp}를 추가합니다.')
          slope.append(temp)
        else: #temp in slope
          print('이미 기울기가 있네요!')
          pass
  return len(slope)*2-1



#마지막에 0,0 은 포함하지 않지만 우리는 포함되어있으므로 -1 해줘야함.



C=int(input())
for _ in range(C):
  N=int(input())
  print(no_see_cnt(N))

시간을 무진장 초과하는 브루트포스 코드를 완성했다.

생각을 잘했기 때문에 스스로도 칭찬하지만, 기울기가 존재하면 범위 내에서 그 기울기인 애들을 미리 제거하는 방식을 취해야한다.

 

[정답코드]

DP와 최대공약수 개념 사용

def gcd(i, j):
    if j == 0:
        return i
    return gcd(j, i % j)

dp = [0 for _ in range(1001)]
dp[1] = 3
for i in range(2, 1001):
    cnt = 0
    for j in range(1, i+1):
        if i == j:
            continue

        if gcd(i, j) == 1:
            cnt += 2
    dp[i] = dp[i-1] + cnt


T = int(input())
for _ in range(T):
    N = int(input())

    print(dp[N])

(0,0)에서 보이지 않으려면 직선의 기울기가 달라야 한다.

이를 다르게 생각하면, (a,b)에서 a,b를 나누어 뜨리는 약수가 존재하는 경우, 둘이 서로소가 아닌 경우 볼 수 없는 자리다.

=> a,b가 서로소인 경우 볼 수 있는 자리다! 그리고 cnt+=2 인 이유는, 대칭으로 두 개씩 증가하기 때문이다!

4/2나 6/3은 약분하면 둘 다 기울기가 2로 똑같다. 따라서 분자와 분모의 최대공약수가 1인 값들을 찾아내면 된다.

 

좌표끼리 분수를 만들고 약분되면 그 좌표는 보일 수가 없다.  
이전에 이미 같은 기울기가 나왔다는 뜻이기 때문이다. 
따라서 분모와 분자의 최대공약수가 1일때만 더해나가면된다.

 

 

출처: https://sangminlog.tistory.com/entry/boj-2725 [로그 남기기:티스토리]

#대칭성이 있는지 확인하기 : 다 구하지 않고, 반만 구해도 된다.
#변화가 없다면 누적합이 가장 빠르다.

 

1.시간복잡도

1억(10^8)번이 1초의 시간이 걸린다고 생각한다.

2. 2805 나무 자르기

[문제]

1.상근이는 나무 M미터가 필요하다.

2.상근이는 절단기에 높이 H를 지정해야한다. 높이를 지정하면 톱날이 땅부터 H미터 위로 올라간다. 그리고 한 줄의 연속된 나무(자를 수 있는 나무) 전체를 절단해버린다.

3.높이가 H보다 크면 H위의 부분이 잘릴 것이고, 낮은 나무는 잘리지 않는다.

4.절단기 설정 가능 높이 >= 0

5.적어도 M미터의 나무를 가져가기 위한, 설정 높이 H의 최댓값 구하기.

 

[입력]

1.첫째 줄에 나무의 수 N과 필요한 나무 길이 M이 주어진다. ex) 4 7

2.둘째 줄에 나무의 높이가 주어진다. 나무의 높이들은 항상 >=M(필요한 나무 길이) 따라서 항상 가져갈 수 있음.

ex) 20 15 10 17

 

[첫 번째 문제 풀이] - 틀린 코드

N, M = map(int, input().split())

trees=list(map(int, input().split()))

trees.sort(reverse=True)



if len(set(trees))==1:
  if M==0:
    height=0
  else:
    for i in range(1, len(trees)):
      if len(trees)*i == M:
        height=i
        break
else:
  step=0
  height=0

  for i in range(len(trees)-2):
    if M==0:
      break
    start = trees[i]
    end = trees[i+1]
    step+=1
    for j in range(start-end):
      if M==0:
        break
      M=M-step #이때 step은 획득되는 길이
      height+=1
print(trees[0]-height)

위 코드대로 써보면, 진행 과정도 찍혀서 이해하기 쉽다.

문제를 푼 순서 로직은 다음과 같다.

1.먼저 나무를 내림차순으로 정렬해준다.

2.나무 길이를 앞에서부터 반복문으로 start, end로 잡고, 비교 횟수를 step으로 잡아준다. 이 비교횟수는, 해당 구간에서 1cm 이동할 때마다 1*step cm 만큼씩 얻을 수 있다는 것을 의미한다.

나무 길이를 첫 번째 예시 케이스인 20, 17, 15, 10 으로 가정해보자.

20  
  이 구간 동안은 1cm가 1cm를 의미(구간에 있는 나무 1개)
17  
  이 구간 동안은 1cm가 1*2cm를 의미 (구간에 있는 나무 2개)
15  
  이 구간 동안은 1cm가 1*3cm를 의미 (구간에 있는 나무 3개)
10  
  이 구간 동안은 1cm가 1*4cm를 의미 (구간에 있는 나무 4개)

 

[배운점]

1..sort(reverse=False)가 디폴트이다. 내림차순 정렬하려면, .sort(reverse=True) 해주면 됨.

2.하나하나 모든 케이스를 따지는 경우에는, 예외 사항을 처리하기가 굉장히 까다롭다ㅠㅠ...하 왜 틀리는건지 답답해 죽겠네. 

3.나의 잘못된 코드로 수많이 오류가 났고, 시간을 낭비했다. 물론 그 안에서 배우는 것은 많았으나, 시간을 들여서 푸는 것은 일단 어느정도 알고리즘 개념과 필수 부분들을 암기한 후에 그때 시간들여서 푸는 것으로 하자. 이론책 다 보고 문제 보려면 시간 너무 오래 걸린다. 일단 개념부터 알자.

 

분할정복

거듭제곱을 1억번 이상하기엔 너무 계산이 오래 걸린다. 이때, 거듭제곱을 이진수를 활용하여 계산할 수 있다.

시간복잡도, 공간복잡도 

C+에서는 int보다는 long long, Java에서는 int 대신 long 쓰는게 둘 다 메모리가 4Bytes -> 8Bytes 2배이기 때문에 그냥 이것 쓰는게 마음 편하다. 부동 소수형도 float말고 double 쓰는게 마음 편하다. 실제 코딩 테스트 등에서는 이 점 반드시 지켜서, 어이없게 탈락하는 일 없게 하자. 단, overflow만 주의하고, 자신없으면 long을 쓰자.

동적계획법 계산할 때

첫 항이 반드시 하나가 아니다. 두 개, 세 개 일 수도 있으니 유의할 것.

top-down으로 먼저 연습하고, 익숙해지면 bottom-up으로 연습하자.

메모이제이션 - 동적계획법 코드 짜는 법

1) 기저 조건

2) 점화식 : 점화식을 찾는 것이 어렵고, 알고리즘 공부를 하지 않아도 풀 수 있다. 식을 찾아내는 것이 어렵다.ㅁ

3) 메모이제이션으로 저장해 줌

2517 달리기

merge sort(합병 정렬)은 분할 정복 알고리즘 중 하나입니다.

분할 정복은 문제를 작은 2개의 문제로 분리하고, 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략입니다. 

자료구조

List vs Linked List

Linked List의 경우에는 반드시 암기해줘야함.

 

java할 때는 배열 잡을 때, 10만 100만 작은 것을 앞으로 두기.

 

int[][] arr1 = new int[100][10000];

int[][] arr2 = new int[10000][100];

앞에가 작은 것이 훨씬 더 효율적이다. 정답여부까지 바뀔 수 있으니 조심하기!

 

트리 구조 코드를 짤 때, 사향 이진 트리(연결리스트처럼 한 줄로 연결되어 있는 트리)인지 확인이 필요하다.

 

-이진 트리를 표현하는 두 가지 방법

1) 연결 자료 구조 : 연결 리스트로 class 를 통해 구현 가능

2) 연속 구조 : 일차원 배열 활용하여 표현 가능(교재 참고하기, 40페이지)

 

문제에서 N이 10~30만이다 => NlogN 선형로그형

N이 5천정도면 2차형 알고리즘

N이 30정도다 => 팩토리얼형 등이다.

 

N의 수에 따라서 알고리즘 유추 가능!

 

이진트리 굉장히 중요하고, 이진트리는 4가지 형태가 있다.(교재 참고)

정 이진트리, 포화 이진트리, 완전 이진 트리, 사항 이진 트리

1.sort() 정복하기

-리스트 안에 담긴 것들은 모두 기본으로 오름차순 정렬이 된다.(숫자, 문자, 문자열 전부 상관없이)

-아스키코드 기반으로 정렬하는 것이므로, 대문자가 소문자보다 먼저 오게 된다.

-내림차순을 하고 싶으면, reverse=True 를 속성으로 넣어주면 된다.

-2차원 배열부터는 어떤 것을 기준으로 배열할지 기준을 정해줘야한다. 이때 람다 함수를 사용한다.

c = sorted(a, key = lambda x: x[0]
#[(0, 1), (1, 2), (3, 0), (5, 1), (5, 2)]

d = sorted(a, key = lambda x : x[1])
# [(3, 0), (5, 1), (0, 1), (1, 2), (5, 2)]

#비교할 아이템이 요소가 복수 개일 경우, 튜플로 우선순위를 정해준다.
#게다가 -를 붙이면, 현재와 반대차순으로 정렬된다.

e = sorted(a, key= lambda x : (x[0], -x[1]))
#[(0, 1), (1, 2), (3, 0), (5, 2), (5, 1)]

f = sorted(a, key = lambda x : -x[0])
#[(5, 1), (5, 2), (3, 0), (1, 2), (0, 1)])

-sort(key=len) 이라고 하면, 문자열을 알파벳 순서대로 정렬해준다.

python 1181 단어정렬(실버5)
 #1181 단어 정렬

N = int(input())
words=[input() for _ in range(N)]
words=set(words)
word_tuples=[]
for word in words:
  word_tuples.append([word,len(word)])
word_tuples.sort(key= lambda x : (x[1], x[0]))

for word in word_tuples:
  print(word[0])
n = int(input())
lst = []

for i in range(n):
    lst.append(input())

lst.sort()	## 괄호 안에 아무 값도 넣지 않으면 알파벳 순서대로 정렬을 해 준다.
lst.sort(key = len)	## 문자열 길이 순으로 정렬.

for i in lst:
    print(i)

조합은 파이썬에서 라이브러리를 제공해주는 것으로 알고 있다.

 

import math

# nCr 계산
n = 5
r = 3
result = math.comb(n,r)

 

Q1.숫자를 list()로 감싸는 것은 에러가 나는가?

O. 에러가 난다. 주의할 것.

 

따라서 문제에 대한 답 코드는 다음과 같이 작성하였다.

import math, sys

def input():
    return sys.stdin.readline().rstrip()

n,m=map(int, input().split())

result = math.comb(n,m)
result_list=list(str(result))
cnt=0

for i in reversed(result_list):
    if i=='0':
        cnt+=1
    else:
        break
print(cnt)

 

하지만 리스트 연산이 많아서 그런지 시간 초과가 떴다.

import math, sys

def input():
    return sys.stdin.readline().rstrip()

n,m=map(int, input().split())

result = math.comb(n,m)
cnt=0

for i in str(result)[::-1]:
    if i=='0':
        cnt+=1
    else:
        break
print(cnt)

 

흠.. 이렇게 했는데도 시간 초과가 뜬다. 그러면 어떻게 하지?

 

일단 개선 방안을 생각해보았을 때, math.comb(n,m) 이 함수가 생각보다 시간이 오래걸리는 듯하다.

직접 만들어서 쓰는게 훨씬 효과적일 듯 하다.

 

일단 팩토리얼 함수 만들어보자.

def factorial(num):
    result=1
    for i in range(2,num+1):
        result *=i
    return result

조합 식은 다음과 같은데, 

우선 조합을 코드로 표현해보겠다.

def combination(n,k):
    #예외 처리
    if k > n:
        return 0
    if k == 0 or k==n:
        return 1
    
    #팩토리얼 계산
    def factorial(num):
        result=1
        for i in range(2,num+1):
            result *=i
        return result
    
    #이때 조합의 계산은 항상 정수이기 때문에, 정수 계산을 위해서 /가 아니라 //를 사용한다.
    result = factorial(n)//(factorial(n-k)*factorial(k))

    return result

 

이때 k!과 n-k!에서 중복되는 부분이 상당히 생긴다.

이 부분을 한 번 메모이제이션으로 해결해보자.

 

열심히 아래와 같이 코드를 제작하였다.

import sys

def input():
    return sys.stdin.readline().rstrip()

def comb(n,k):
    if k > n:
        return 0
    if k==0 or n==k:
        return 1
    
    #k가 n/2보다 큰 경우, nCk=nC(n-k)와 같음. 더 작은 수로 바꿔주는 과정
    if k > n/2 :
        k = n-k

    result=1
    for r in range(k): #r-1까지 돌 것임
        result = result * (n-r) // (r+1)

    return result

n, r = map(int, input().split())
print(comb(n, r))

 

하지만 여전히 에러가 났다 ^_^ 한 단계 더 가야하나보다.

파스칼의 삼각형을 이용하는 방법도 생각하였으나, 정답을 보니 0의 개수는 2와 5의 개수로 판단하는 것 같다.

 

팩토리얼에서 특정 소인수의 개수를 계산하는 방법

 

1.모든 2의 배수를 카운트한다.

: n을 2로 나눈 몫은 n 이하의 모든 수 중 2의 배수의 개수를 의미한다.

몫 = n이하의 2의 배수들의 개수

이게 잘 이해가 안됐었는데, 나열해놓고 세어보면 결국 그렇다. 그냥 외우자.


예를 들어, n=10이면 10//2=5로, 2 4 6 8 10 총 5개의 숫자가 하나 이상의 2를 갖고 있다.

 

이때 코드는 두 가지 관점에서 짤 수 있다. 결국 결과는 같다.

1) 나누는 수를 2, 4, 8 ...로 높인다.

2) 주어진 n을 2로 나눠가면서 반복 계산한다.

 

원리를 이해하기에는 1번이 더 쉽지만 코드 짜기에는 2번이 더 쉬운 듯 하다.

드디어 정답 코드

 

import sys

def input():
    return sys.stdin.readline().rstrip()

def cnt(s,n):
    if n < s:
        return 0
    cnt=0
    while n>=s:
        cnt +=n//s
        n=n//s
    return cnt

n,m = map(int,input().split())
two_cnt = cnt(2,n) - cnt(2,n-m) - cnt(2,m)
five_cnt = cnt(5,n) - cnt(5,n-m) - cnt(5,m)

print(min(two_cnt,five_cnt))

 

 

+) sys.stdin.readline()과 sys.stdin.read()의 차이

이때 sys.stdin.readline()과 sys.stdin.read()의 차이가 궁금해서 찾아봤다.

-read()

: 여러 줄 입력을 처리할 때 사용된다. 따라서 메모리 사용량이 많아질 수 있으나, 빠르게 많은 양의 데이터 읽기 가능하다.

 

-readline()

: 한 줄씩 입력을 처리한다. 따라서 메모리 사용량이 상대적으로 적으나, read()보다 느리다.

1. 원격 레포지토리에서 브랜치 지우는 명령어

git push origin --delete main

위의 명령어는 로컬 브랜치는 여전히 남아있다. 이때는 로컬 브랜치도 지워주자

git branch -D main

 

 

'git' 카테고리의 다른 글

github default branch 변경하는 법  (0) 2024.06.18

1. 레포지토리의 settings로 들어갑니다.

 

 

2. branch 선택하고 update 클릭

3. 버튼 클릭

'git' 카테고리의 다른 글

git branch 각종 명령어 - 브랜치 지우기  (0) 2024.06.18

1. 시간 초과 코드

: people 이라는 리스트에 전부다 넣고, 길이가 2이면 새로운 find_people 리스트에 넣어서 하나씩 출력하는 형태의 코드를 작성했다. 당연히 시간 초과!

import sys
def input()->str:
    return sys.stdin.readline().rstrip()

n, m = map(int, input().split())
#n 듣도 못한 사람의 수
#m 보도 못한 사람의 수

people=[]

for i in range(n+m):
    people.append(input())
num=0
find_people=[]
while(len(people)!=0):
    tem_people=people[0]
    if people.count(tem_people)==2:
        find_people.append(tem_people)
        del people[0]
        num+=1
    else: #개수 하나
        del people[0]
print(num)
find_people.sort()
for j in find_people:
    print(j)

 

2. 두 리스트에서 공통인 것만 출력하는 기능을 활용한다. 교집합 개념 활용!! (성공)

import sys
def input()->str:
    return sys.stdin.readline().rstrip()

n, m = map(int, input().split())
#n 듣도 못한 사람의 수
#m 보도 못한 사람의 수

people_who_dont_hear=[]
people_who_dont_see=[]

for i in range(n):
    people_who_dont_hear.append(input())

for j in range(n):
    people_who_dont_see.append(input())

intersection = list(set(people_who_dont_hear) & set(people_who_dont_see))
intersection.sort()
print(len(intersection))
for i in intersection:
    print(i)

 

완성 코드. 브루트포스. 생각보다 아직 어려워서 다른 코드들을 참고했다.

문제 이해도 너무 오래 걸리고, 푸는 데도 너무 오래 걸려서 속상하다.

그래도 점점 늘겠지? 다양한 인사이트들을 얻었고, 가장 많이 참고한 정답 코드 블로거보다 내가 더 짧고 가독성 좋게 입력한 부분도 있었다.

그리고 브루트포스라는 힌트만 얻고나서, 그래도 나름 맞게 코드를 작성했었다. 오류들이 많아서 그렇지...

 

문제 꼼꼼하게 읽자..!

import sys
def input()->str:
    return sys.stdin.readline().rstrip()

n, m, b = map(int, input().split()) #세로 n, 가로 m, 인벤토리에 99개의 블록
heights=[list(map(int, input().split())) for _ in range(n)]

#블록을 추가하거나, 빼는 방법 2가지 경우 스킬 존재
#약간 체스판 자르기랑 비슷한 문제인 것 같다.

# 브루트포스로 모든 경우의 수를 구하고, 가장 시간이 적게 걸린 것 중,
# 땅의 높이가 높은 것 출력.

# 절대값 구하는 함수 abs()

ans = int(1e9) #답을 무한으로 걸어놓고, 조건을 만족시키면 바꿔준다.
glevel=-1
for h in range(0, 257):
    take_block=0
    use_block=0
    for i in range(n): #세로(row)
        for j in range(m):#가로(column)
            if heights[i][j]>=h: #추가
                take_block+=heights[i][j]-h
            else: #heights[i][j] <h:
                use_block+=h-heights[i][j]
    if use_block > take_block + b:
        pass
    else:
        t_sum=take_block*2+use_block

        if t_sum <= ans:
            ans = t_sum
            glevel=h #어차피 점점 높이가 높은 것으로 갱신 되니까
print(ans,glevel )

아래는 문제를 풀면서 얻은 내용들이다.

 

[.index() 특징]

.index()는 중복값이 있을 경우, 가장 큰 값을 리턴한다.

 

[높이 구하는 방법]

일단 초기값을 glevel = -1로 지정해 놓은 뒤, 어차피 높이가 올라가면서 가장 높은 것으로 자동으로 갱신되도록 했다.

 

[최댓값, 최솟값 리스트 쓰지 않고 계산하기]

매번 계산한 다음, 계산한 값들 중 최대값을 구할 때는 항상 list에다가 넣은 후, max(list)를 하는 방식으로 계산했었는데,

그게 아니라 초기값을 0으로 잡아둔 뒤(최소값 구할 때는 int(1e9)로 잡기) 하나의 값을 지정하고, 이 값보다 작을 경우 업데이트 하는 방식으로 가능.

 

드디어 Class 2 문제 다풀었다. 야호 !

박은 캠프 때 n개의 랜선을 만들어야한다.

오에게 도움을 청했고, 오는 k개의 랜선을 가지고 있는데, 길이가 제 각각이다.

박은 랜선을 모두 n개의 같은 길이 랜선으로 만들고 싶다.

이때 최대 랜선의 길이를 구하는 프로그램을 만들어라.

 

알고리즘을 활용해야하는 문제 같다. 이진탐색 알고리즘을 사용하는 것으로 판단되지만, 일단 인간이 접근하는 방법을 토대로 풀어보고 싶었다.

 

1. 첫 번째 풀이 : 내 마음대로 풀이(테스트케이스 통과O but 시간 초과 코드)

그래서 우선은 나에게 수학적 문제로 주어졌다면 어떻게 풀 수 있을까 생각해봤다.

 

만약 k=n을 요구했다면, 당연히 가장 길이가 짧은 랜선의 길이가 result가 될 것이다.

그리고 실제로 인간의 감각(훈련된 수학적 사고)로 어떻게 하는지 생각해봤는데,

내림차순으로 lines를 정렬하고,

만약 k+1<=n가 시작되면

반복문은, 큰 수의 1/2부터 넣어보며, 몇 개의 조각이 나올 수 있는지 계산해본다.

그리고 가장 작은 수까지 반복문을 돌았는데 원하는 n에 접근되지 못하면,

 

큰수의 1/4부터 넣어보며, 전체에서 총 몇 개의 조각이 나올 수 있는지 계산한다.

이렇게 했을 때, 미세한 조정은 조금 더 필요하겠으나 대략적인 접근과 예제 입력은 만족시킬 수 있게 된다.

import sys, math
input=sys.stdin.readline

k, n= map(int, input().rstrip().split())
#k개의 랜선
#를 n개로 모두 같은 길이로 만드세요
#n개를 만들 수 있는 랜선의 최대 길이 출력

lines=[int(input()) for _ in range(k)]

lines.sort(reverse=True) #오름차순으로 정리
    #lines는 내림차순 정렬 이후 건들이기 없기

cnt=0 #몇 번째 순회인지 확인
breaker1=False
breaker2=False
while(True):
    if breaker1 == True and breaker2 == True: #삼중반복문 탈출
        break
    #밖에서 Lines를 변형해준다.
    standard_lines = [math.floor(line*((0.5)**cnt)) for line in lines] #변경 가능한 조건
    for standard in standard_lines: #line은 나누는 기준 line
        #이중반복문 탈출
        if breaker1==True: 
            breaker2=True
            break

        q_sum=0 #만들어지는 그룹의 개수
        for line in lines:
            q=line //standard
            q_sum+=q
        if q_sum == n:
            print(standard)
            breaker1=True
            break
    cnt+=1

그래도 바로 틀렸어요!가 아니라 시간초과가 떠서 그나마 기분이 좋다.

왜냐하면 종이에다가 쓰고, 블로그에다가 글을 쓰면서 계산하니까

술술 풀린 문제이고, 스스로도 뿌듯했기 때문이다.

 

반복문이 너무 많은 게 문제인 것 같아서

standard_lines를 매번 반복문으로 돌리는 부분을 좀 어떻게 해볼까 싶다.

 

해결하기가 쉽지 않아서, 카카오톡 오픈 채팅방에다가 물어보기도 했는데

이렇게 맞닿은 상황에서는, 로직 자체가 잘못된 경우에는 빠르게 바꾸는 것이 좋다고 한다.

2. 이진 탐색을 이용한 풀이

먼저 아직 이진 탐색 풀이에 익숙하지 않기 때문에, 이전에 풀었던 문제부터 복습해보겠다.

 

1920 수찾기 문제에서 풀었던 이진탐색 코드는 다음과 같다.

def binary_search(a_list, x):# a는 탐색 대상 리스트, 찾아야 하는 수 x
    start=0
    end=len(a_list)-1

    while(start<=end):
        mid=(start+end)//2

        if a_list[mid] == x:
            return 1
        elif a_list[mid] < x:
            start= mid+1
        else: #a_list[mid] > x:
            end = mid -1
    return 0

 

최종적으로 작성한 코드는 다음과 같다.(정답 코드)

import sys
def input()->str:
    return sys.stdin.readline().rstrip()

#이진탐색
def binary_search(a_list, x):# a는 랜선 리스트, x는 입력한 n
    start=1 #가장 짧은 길이
    end=a_list[-1] #가장 긴 길이

    while(start<=end):
        print('현재 start=',start,'현재 end=', end)
        mid=(start+end)//2 #결국 최종적으로 mid가 문제에서 찾고자 하는 정답이 될 것임
        result=0
        print('현재 mid=',mid)
        for line in a_list:
            result+= line // mid
        print('현재 result=',result)
        if result >= x:
            start = mid+1
        elif result < x : #개수가 x보다 적으면,개수를 늘릴 수 있도록 해야하니까 정답인 mid를 더 작게 바꿔야한다.
            end = mid -1
        # else: #result > x: #개수가 x보다 많으면, 개수를 줄일 수 있도록 정답인 mid를 키운다.
        #     start= mid+1
    return end

k, n = map(int, input().split())
#k개의 랜선을 n개로 모두 같은 길이로 만드세요
#n개 만들 수 있는 랜선의 최대 길이 출력(answer)

lines=[int(input()) for _ in range(k)]

lines.sort() #오름차순으로 정리

print(binary_search(lines, n))

처음에는 이분탐색 함수 내에서,

if result == x:
	return mid
elif result > x:
	start = mid + 1
else: #result < x:
	end = mid -1

 

이렇게 조건문을 작성했다. 정답 코드에서는 end를 출력하는데, 내 오답에서는 mid를 출력했다.

약간은 이해가 덜 되긴 했는데, start가 아닌 mid인 이유는, 마지막에 start는 while문에서 조건식을 초과하기 때문에 end로 출력한 것이고, while문이 모두 끝나야 최선의 답을 찾을 수 있기 때문에, while문이 모두 끝난 뒤의 값을 출력하는 게 예외사항 없이 맞는 듯하다.

 

이진탐색(이분탐색)에 대해 확실히 공부할 수 있어서 좋은 문제였다.

 

 

+) 추가 공부 : 이중 반복문에서 break해서 나오는 방법

맨날 코드를 짜다가 이러한 상황에 마주해서 당황한 적이 많았는데, 한번 정리를 하고 가는게 맞는 것 같다.

1) 불린형 조건 활용

breaker = False로 기본값으로 주어주고, 첫 번째 반복문을 나올 때 breaker=True로 바꾸면서 break한다.

그리고 두 번째 반복문에서는 breaker가 True이면 정지하도록 걸어둔다.

 

2) 예외처리 try: except: 문 활용

try: 이중 반복문

except :
	#오류 발생시 실행할 문구 작성

와 같이 코드를 짜면, NotImplementedError(선언되지 않은 에러)를 발생시켜, 다중 for문을 바로 종료시킨다.

그런데 이 방법보다는 1번 방법이 더 선호되는 것으로 알고 있다. 너무 많이 쓰면 가독성이 떨어지고, 코드 실행속도가 느려질 수 있기 때문!

 

이외 방법은 아래에서 참고 가능하다.

try:
    # 예외 발생 가능한 코드
    result = some_function()

except ValueError:
    # ValueError 예외 처리
    print("ValueError occurred")

except IndexError:
    # IndexError 예외 처리
    print("IndexError occurred")

except Exception as e:
    # 그 외 모든 예외 처리
    print("An error occurred:", e)

finally:
    # 예외 발생 여부에 상관없이 실행되는 코드
    print("Execution complete")

위 코드 블록 내용 출처 : https://twoicefish-secu.tistory.com/520

 

1. 해시함수란?

임의의 길이의 입력을 받아서, 고정된 길이의 출력을 내보내는 함수.

보통 자료의 저장에도 사용 되지만, 주로 매우 빠른 데이터 탐색에도 사용된다.

 

2. 해시함수 알고리즘이란?

긴 길이의 데이터를 짧은 길이의 데이터로 변환하는 알고리즘이다.

단, 제 3자는 짧은 길이 데이터로부터 긴 길이의 데이터를 도출할 수 없어야한다.

그리고 동일한 출력을 갖는 데이터를 찾을 수 없어야한다.

 

공개키 암호 알고리즘 RSA가 해시함수의 일종이지 않을까 생각해봤다.

그러나 공개키 암호 알고리즘은, 긴 길이의 데이터를 짧은 길이의 데이터로 변환하는 것은 아니니, 완전히 같지는 않고

제 3자로부터 암호화 전의 비밀번호를 알지 못하게 하는 '보안'에서 사용된다는 공통점이 있겠다.

 

3. 문제 풀이

영어 알파벳은 26개 이므로, a~z에 1~26 숫자를 부여한다.

    따라서 문자열을 입력하면, 수열을 얻을 수 있다.

이렇게 얻은 수열을 하나의 정수로 치환하기 위해서, 적당히 큰 수 M으로 나눠주겠다.

M으로 나눴을 때의 나머지를 H(출력 값=해시 값)이라고 하겠다.

 

나누는 수가 생긴다는 것은, 나눴을 때 나오는 나머지의 범위로 범위가 정해진다.

    (비둘기집의 원리-27로 나누는데, 전체 모수가 28개 이상이 되면 비둘기집의 원리에 의해 겹치는 쌍이 1개 이상 발생한다.)

이렇게 동일한 해시 값을 가지는 것을 '해시 충돌'이라고 하고, 좋은 해시 함수는 해시 충돌의 수가 적다.

 

지금까지의 해시함수 암호화 방법은

1) a~z에 1~26 숫자 부여

2) 다 더한 뒤, 적당히 큰 수 M으로 나눈다.

이 방법이다. 그런데 이것은, abc와 acb와 같이 순서가 달라져도 같은 값을 가지므로 해시 충돌이 발생한다.

 

따라서 이 방법을 개선하기 위해서, 각 항마다 고유한 계수를 부여한다.

특정한 숫자 r을 i번째면 i만큼 곱해준다.(0번째 수는 r^0, 1번째 수는 r^2)

r과 M은 서로소로 정하는 것이 일반적이다.

이 문제에서는 r=31, M=1234567891로 가정한다.(둘 다 소수임)

 

4. 코드 작성 순서

1. a~z를 1~26으로 전환하는 코드 작성

2. 반복문으로 문자열의 위치에 따른 계수들을 곱해줌

3. 합을 구하고, M으로 나눈 H값 출력

 

1. a~z를 1~26으로 전환하는 코드 작성

우선 아스키 코드를 활용해보면 좋을 것 같다.

a=97이므로 아스키 코드 수로 바꾼 후, -96을 해줬다. 그리고 문자로 바꾼 뒤, new_string이라는 문자열에 더해주는 반복문을 작성했다.

이렇게 하면 수열로 바뀐 것을 출력할 수 있다.

import sys
input=sys.stdin.readline

l=int(input().rstrip()) #문자열의 길이
string=input().rstrip() #문자열

r=31 #예시 소수 : 
m=1234567891 #예시 소수 : 나누는 수

new_string=''
for chr in string:
    new_string+=str(ord(chr)-96)

 

2. 반복문으로 문자열의 위치에 따른 계수들을 곱해줌

수열을 출력하는 것이 아니라, 한번의 반복문으로 해결 시키기 위해서, for문 안에서 바로 r을 곱한 후의 sum 값을 가져오도록 코드를 짰다.

import sys
input=sys.stdin.readline

l=int(input().rstrip()) #문자열의 길이
string=input().rstrip() #문자열

r=31 #예시 소수 : 
m=1234567891 #예시 소수 : 나누는 수

sum=0
i=0
for chr in string:
    sum+=(ord(chr)-96)*(r**i)
    i+=1

3. 합을 구하고, M으로 나눈 H값 출력 🚀 정답 코드

자 이제 정답만 구하면 끝난다. 정답이다.

import sys
input=sys.stdin.readline

l=int(input().rstrip()) #문자열의 길이
string=input().rstrip() #문자열

r=31 #예시 소수 : 
m=1234567891 #예시 소수 : 나누는 수

sum=0
i=0
for chr in string:
    sum+=(ord(chr)-96)*(r**i)
    i+=1

result=sum % m
print(result)

 

+ Recent posts