알고리즘

backtracking

backtracking으로 가장 유명한 문제는 N-Queen 문제

[[##]] Shortest Path(최단경로 알고리즘)

Shortest Paths의 조건과 경우들

  • 조건
    • 간선 가중치가 있는 유향 그래프
    • 무향 그래프는 각 간선에 대해 양쪽으로 유향 간선이 있는 유향 그래프로 생각할 수 있다
    • 즉, 무향 간선 (u,v)는 유향 간선 (u,v)와 (v,u)를 의미한다고 가정하면 된다
  • 두 정점 사이의 최단경로
    • 두 정점 사이의 경로들 중 간선의 가중치 합이 최소인 경로
    • 간선 가중치의 합이 음인 싸이클이 있으면 문제가 정의되지 않는다
    • 모든 쌍 최단경로
      • 모든 정점 쌍 사이의 최단경로를 모두 구한다 -> 플로이드-워샬 알고리즘
    • 단일 시작점 최단경로
    • 단일 시작점으로부터 각 정점에 이르는 최단경로이면서 음의 가중치를 허용하지 않는 최단경로 -> 다익스트라 알고리즘
    • 음의 가중치를 허용하면서 싸이클이 없는 그래프의 최단경로 -> 벨만-포드 알고리즘

하노이탑

if) n=3

  1. from - tmp - to

    1. from에 있는 1, 2를 tmp로 옮김

    2. 들어가보면 from - to - tmp가 됨. a의 tmp로 가야되니까.

      1. 1이 tmp로 가고
      2. 2가 to로 갓다가
      3. 다시 tmp 1이 2의 to로 옴.
    3. 그러면 tmp - from(1,2) - to(3)가 됨

결론 위에 있는 모든 덩어리를 tmp에 놓고 맨 밑 원판을 to에 옮기고, tmp에 있던 덩어리를 다시 to로 올린다.

비트마스킹

&, , «, »>, », ^, ~ 비트연산은 정수형만 할 수 있다.

● & - 각 자리의 모든 비트를 and 연산하는 것.

○ 11에서 둘째자리 1이 1인지 0인지 알고싶을 때 알고싶은 자리를 1로하고 나머지를 0으로 했을 때 &을 해서 1이 나오면 1이니까 알 수 있다. 0이면 아무리해도 0이니까.

- 논리합 or

상대 비트가 1인지 0인지 알아낼 때 쓴다.

켜져있는지는 and로 판단, 사용중이지 않아서 쓰기로 결정해서 내가 원하는 곳을 켜주고 or 연산.

1«0 1을 0만큼 shift. 0001

1«1 이면 0010

1«2 이면 0100

쉬프트 연산자 즉, 비트마스킹으로 해서 할 수도 있다. 그런데 int보다 클 수도 있어서 BigInteger를 사용해야 한다.

BigInteger는 객체라서 +나 -를 못하고 다 메소드로 이루어져 있다.

BigInteger는 매개변수가 스트링이다.

and 연산을 했을 때 각 자리의 비트가 켜져있는지 확인해서 0이 나오면 꺼져있는 것.

1208번 문제

● 카운팅 배열로 풀어보기

● 0인덱스 안 쓰고 1인덱스부터 차례대로 만들기.

● 예시에 나온 5 8 3 1 5 6 9 9 2 2 4 를 카운팅 배열로 정리하면

● 1 2 1 1 2 1 0 1 2 가 나온다.

● min = 1, max = 9;

● 9짜리 박스를 줄이면 8로 되서 8짜리가 1개 더 늘어난다.

● 1 2 1 1 2 1 0 2(8) 1(9)

● 1이 없어지고 2가 늘어남.

● 0(1) 3(2) 1 1 2 1 0 2(8) 1(9)

● 원래있던 min이 0이 되거나 max가 0이되거나 하면 바꿔야된다.

● min = 2, max = 9;

높이가 중요하지 순서는 중요하지 않음.

● 또 진행하면 0(1) 3(2) 1 1 2 1 0 3(8) 0(9)

● min = 2, max = 8;

● 받았으니까 0(1) 2(2) 2(3) 1 2 1 0 3(8) 0(9)

● 덤프 횟수만큼 min, max. 하면 됨.

PDF 23 - babygin 풀이 방법

  1. 진짜 완전탐색

    1. 6중 포문을 써서 모든 경우의 수를 판단한다.
  2. 인덱스순열

    1. 인덱스 순열을 미리 만들어놔서 거기다 대입하는 방법

테케 수 만큼 돌리면 테케 하나에 720번째에 걸림.

012 인덱스가 있다면 012, 021, 102 … 6가지가 있다.

ex) 135라면 135, 153, …513… 등.

인덱스 순열을 먼저 만들어놓고, 각 숫자가 입력으로 들어오면 그 개수만큼 시도.

a[0] +1 == a[1] && a[0]+2 == a[2] run++

a[0] == a[1] && a[1] = a[2] triplet++

a[3]+1 = a[4] && a[3] + 2 == a[5] run++;

a[3] == a[4] && a[3] == a[5] triplet++

if(run+triplet == 2)

6개로 구분되어있으면 반복문으로 굳이 안 해도 됨.

  1. +1 된 것이 다음 거일 때 한번씩 확인하는 방식.
  2. 카운팅 배열로도 풀 수 있음. 가장 효과적. 근데 막상 떠오르진 않을듯.

pdf 34

● n개중 r개. 처음에 n개*n-1가지… n - (r-1)

순열, 부분집합은 기본적으로 마스터하고 있어야할 내용들.

그리디는 쉽지 않은 알고리즘. 왜냐면 증명이 되어야 함. 문제가 비중이 높지 않음.

지그재그 순회 - 홀수일 때는, 짝수일 때는 ~ 이런 식으로 하는 게 가장 편함. 가독성도 좋고.

숙제 로봇 이동거리 했던 거 그거 델타 배열 딱 하나만 써서 해결해보라.

정점의 수 만큼 2차원 배열을 만듦. ABCD, ABCD A가 A로 갈 수 있냐의 방식.

방향성이 있는 그래프가 있고 없는 그래프도 있음.

내가 갈수있는 부분이 행우선탐색. –»쪽으로.

연습문제 델타 모르겠다면 풀기

부분집합

● 1 2 3개 중 선택하거나 선택하지 않는 경우 222

● for 선택 비선택 <- 첫 번째 원소, 또 다시 for로 선택과 비선택 <- 두 번째 원소. 세 번째도 마찬가지.

이분탐색

● 정렬되어야 있어야한다.(전제조건)

● 데이터가 많으면 많을 수록 좋다

● 검색한다기보다 이진탐색을 이용해서 처리 범위를 줄여가는 용도로 많이 쓰인다.

부분집합의 합 부분은 제대로 알고 가기

항상 어떻게 하면 처리 범위를 줄일 수 있을까 생각해야됨.

hashcode는 객체의 유일한 해쉬를 만들 수 있으니 재정의를 해주면 좋다.

정렬되어있으면 중간에 끝낼 수 있어서 좋다. 탐색의 알고리즘이 더 간편해진다.

인덱스또한 무식하게 찾지 않는다. 트리구조로 되어있다.

문자열

각 지역별로 달라서 표준이 필요함.

이렇게 쓰면 됩니다~ -> 인터페이스

자바는 2바이트로 구성, 16비트 16진수 4개로 표현.

주소번지는 오른쪽으로 갈수록 높아짐. 리틀엔디언.

문자열이 상수의 성질을 가지고 있어서 새 문자열 객체가 무수히 만들어짐. 이걸 보완하기 위해서 스트링버퍼가 만들어짐.

+는 매번 새 문자열 객체를 리턴.

나란히 실행하는 개념 스레딩. 공유되는 자원을 동기화시켜서 써야함. 공유자원에 대해서 시간적인 공유는 안 되도록. 공간적인 공유는 됨. 스트링버퍼는 동기화가 되어있다. 스트링빌더는 그냥 벌컥벌컥 물어보지않고 방에 들어간다. 그래서 멀티스레딩환경에서 스트링빌더는 안된다. 싱글스레딩환경에서만 스트링 빌더를 써야한다.

문자열을 계속 조작해야 된다면 스트링 빌더를 써서 중간 과정에 새로운 스트링을 만들면 안 된다.

직접 맞물리는 스트림 : 노드스트림.

JVM에 올라갔을 때 기본적으로 추가되는 스트림 System.in, System.out, System.err

Buffered가 Scanner보다 속도가 훨씬 빠르다.

StringTokenizer 생성자가 string을 받아들이니까. 특별히 구분자를 주지 않으면 공백을 기준으로 함.

concat될 때마다 새로운 문자열로 계속 기억하고 있는 중

원본 문자열에 영향을 안 줌.

그래서 얘를 쓰면 힙을 계속 쓰는 중…

많이 쓴다 - 처리속도 개선. Scanner를 쓰는데 너무 느리다면.

st = new StringTokenizer(in.readLine()); // x y ==> 3 4

​ int x = Integer.parseInt(st.nextToken());

​ int y = Integer.parseInt(st.nextToken());

​ System.out.println(x + “,” + y);

스트링 조작은 빌더를 쓰자.

스택

● 후입선출

● 불필요한 객체는 null로 해도 된다.

한 소스파일에 public이 붙는 클래스는 하나만.

swea_1218

// 스택으로 풀어야 좋은 문제. 가장 마지막에 들어간 녀석을 가장 처음 확인해야 하니까. empty인지도 체크해야된다. 뒤에 여는 괄호가 더 있어도 0이 나와야 한다. 그걸 size로 체크한다.

swea_5432

// 스택으로 풀거나 아니면 그냥 for문.

​ // (가 나오면 무조건 push. ()가 나올 때는 stack.size를 더해주면 됨. )가 나올때는 sum에 +1해주면서 pop.

​ // char[] str = br.readLine().CharArray()를 쓰면 다 차례로 들어온다. 캐릭터 배열로 할 수 있음. 또는 그냥 CharAt(i)로 할 수도 있다.

​ // 여는 괄호면 무조건 스택에 넣기.

​ // 레이저를 빼내면 파이프 수.

​ // 닫는괄호면 일단 pop

​ // === 샘

​ // (이면 쇠막대기 or 레이저

​ // (이면 무조건 push ((((, 닫는 괄호 만나면 무조건 pop,

자료구조

Stack

가장 직전의 작업을 타켓으로 했을 때 스택을 쓴다. 실행중인 메소드는 스택에 쌓임. 싱글스레딩 환경에서는 스택에 따라 다 없어지면(마지막 메인이 끝나면) 프로그램이 끝남.

멀티스레딩은 여러 개가 스택의 top 상태가 됨. 스케줄러가 분할해서 관리해줌.

피보나치 수열

f(n-2)+f(n-1)로 하게 되면 너무나 많은 중복 호출이 발생함.

언제 부르든 3항은 무조건 2. 다르지 않음. n값을 줬을 때 리턴되는 값이 항상 똑같음. 이미 구한 값들.

중복되는 호출이 있는지 보고 똑같은 호출이라면 상향식으로 생각하자.

기억해둔다 - 메모이제이션.

DP

중복되는 호출이 없으면 메모이제이션은 아무런 의미가 없다.

DP(동적 계획법)도 메모리와 시간싸움.

메모이제이션을 재귀적 구조에 사용하는 것보다 반복적 구조로 구현한 게 성능적으로 효율적이다. 재귀는 쌓아야하니까. 쌓다가 끝날 수도 있음.

정올의 1077문제 배낭채우기1같은 경우, 2, 5, 10, 3의 순서로 한 번에 해줘도 되고, 2를 다 한 후, 5 해주고, 10 하고 이런 식으로 해도 된다. 최대 가치니까 MAX값 구해서 찾아주면 된다.

  • 한 정점에서 시작해서 연결된 모든 정점을 1번씩 방문
  • 간선의 가중치가 1일 때 최소 비용/최단 거리를 구할 수 있다
  • 시작점이 있어야 하고, 같은 정점을 두 번 이상 방문하면 안 된다
void bfs(int start){

    Queuequeue<int> queue;
    queue.push(start);
    check[start] = true; // dist[start] = 0; // dist배열을 -1로 초기화해 체크배열로도 쓸 수 있다.
    while(!queue.isEmpty()){
        // queue에서 뺄 때 check을 해주면 안 된다. 왜냐면 큐에 중복된 게 다 들어가고 나서 뺄 때 체크를 해주는 거니까. 넣을 때 check를 해줘야 중복되지 않게 큐에 들어간다.
        int nx = queue.poll(); // queue.front(); queue.pop();
        for(가능한 모든 다음 정점 y){
            if(check[y] == false){ // dist[y] == -1
                queue.offer(y)
                check[y] = true; // 방문배열 검사는 꼭 넣을 때 해줘야 한다. // dist[y] = dist[x] + 1;
                dist[y] = dist[x] + 1;
            }
        }
        
    }
        

}

DFS와 BFS는 트리나 그래프 탐색에서 나온 것.

지금까지는 선형자료구조라 노드들이 다 1:1 구조였음

비선형 구조는 다름. 트리 형태라 한줄로 늘어세울 수 가 없음. 탐색할 수 있는 고민이 필요.

Root를 갖고 있으면 전체를 갖고 있는 거라 생각해도 좋다. 같은 depth끼리 탐색.

큐에 A를 넣고 자식들을 모두 넣음. A(depth0) B1 C1 D2 E2… 순서로 쭉쭉 기다림. depth 순.

같은 depth 끼리 탐색 BFS. 너비우선탐색. 자료구조 중 큐를 씀

DFS는 스택을 씀. 스택에 따라 왼쪽이든, 오른쪽이든… 깊이우선탐색. 정점을 만날 때마다 끝까지 접근. 더 이상 갈 수 없을 때.

끝까지 백트래킹 알고리즘.

중위를 후위표기법으로 바꾸고 연산. 둘 다 스택 이용.

괄호를 해서 페어로 만들어줌.

여는 괄호는 무시하고 연산자가 나오면 Stack1에 넣음. 숫자가 나오면 다른 스택2에 넣음. (2+(3*4))닫는 괄호가 나왔다면 스택1의 가장 마지막에 푸쉬한 얘. 걔를 꺼내서 스택2에 넣음. 다시 닫는 괄호가 나온다면 +를 뒤에 붙임.

234*+ -> 스택1에 2를 넣고 3, 4 그리고 연산자가 나오면 두 개를 팝해서 연산하고 다시 그 결과를 stack2(피 연산자 기억)에 넣음12. 또 연산자 나오면 해주고 마지막에는 결과값. 후위연산자는 괄호가 없어도 연산이 쉽다.

후위표기식을 만드는 건 스택.

만약 괄호가 없다면 더 빡세짐. 전제 조건 등등…

괄호는 연사자의 수 만큼 나와야함?

////

hwalgo05

Pair 라는 클래스를 따로 만들어서 스택에 x, y가 들어가도록 함.

브루트포스로 하면 O(n^2)이라 다른 방법을 모색하는 게 좋음.

스택을 이용하는 방법

비교 횟수를 줄이기 위해 중간에 n-1보다 작은 얘들은 어차피 안 보이니까 탐색 자체를 안 하도록.

class top{

int no; // 네이밍을 정확히 주는 게 좋음.

int height;

원하는 변수를 넣어주고 생성자까지만 추가하자. 바로 대입해주면 좋으니까.

}

s.pop().no로 호출.

스택을 써서 제일 먼저 받을 수 있는 가장 가까운 탑만 봄.

2 4 6 8 10이 있을 때 일단 배열에 넣음. 스택에 있는 얘랑 비교해야 하는데 대상이 없으니까 0을 넣음. result 배열에도 0을 넣음. 4가 올 때 2번을 보는데 안보이니까 0을 pop을 해주고 4를 다시 배열에 넣음. 스택에 1을 넣고 다음 거로 감. …

즉, 스택에는 인덱스가 들어가는데 유효한 1개만 생각하면 된다. result에는

각 위치에서 한 번씩만 비교.

큰 탑은 작은 얘를 가리는데 그런 얘들을 pop 시킨다.

단순하게 먼저 생각해보고 풀기

swea 1222, 1223, 1224

계산기 1번은 다 +니까 우선순위가 같음.

계산기 2번은 +와 *중 *가 우선순위가 더 높아서 먼저 처리하는 방식으로 해야됨

계산기 3번은 괄호가 추가되서 (과 *or/, +or-를 우선순위 고려하면 된다. )는 필요 없음.

  1. stack에는 연산자가 들어감,피연산자는 후위 표기식에 들어간다.
  2. +, -는 1, *, /는 2, (는 두 가지로 나뉘는데 스택 안에 있는 건 0, 스택 바깥에 있는 건 3으로 지정해준다. 사실 스택 바깥에 있는
  3. getPriority(stack, boolean)
  4. getPriority(chars[i], false) <= getPriority(stack.peek(), true)
  5. 괄호가 열린다는 이야기는 우선순위가 최우선.
  6. 연산자 처리하려면
  7. stack의 상단에 있는 연산자와 비교해서
  8. 우선순위가 같은 놈도 먼저 처리를 해야됨
  9. 스택 안에 있는 괄호랑, 스택 바깥에 있는 괄호가 있다. 스택 안에는 최 하위 우선순위, 스택 바깥에 있는 건 최 상위 우선순위.
  10. boolean inInstack?
  11. chars[i],

pdf에 있는 연습문제는 후위표기식이 잘못됨.

백트래킹은 DFS의 방식에 속하는 것.

백트래킹 가지치기를 하면서 최적화

퀵 정렬

반씩 나눠서 피봇을 중심으로 왼쪽과 오른쪽을 정렬해서 놓음. 중간값이나 시작이나 끝값을 피봇으로 해야 오류가 안남.

  1. 피봇 위치 확정 (퀵 정렬의 핵심)

    1. 피봇을 기준으로 왼쪽에는 피봇보다 작은 값
    2. 오른쪽에는 피봇보다 큰 값
    3. 첫째 값, 중간 값, 끝 값 등 피봇은 아무나 될 수 있음.
    4. 분류해서 자기 위치를 찾으면서 부분집합의 크기가 1이 될 때까지 찾음.
  2. 피봇의 왼쪽 집합 정렬(begin, p-1) -> 다시 퀵 정렬로

  3. 피봇의 오른쪽 집합 정렬(p+1, end) -> 다시 퀵 정렬로

  4. if(begin < end) quicksort // 집합의 크기가 2 이상인 것을 보장함. 다시 퀵 정렬로.

시뮬레이션

  1. begin = 0, end = 4
  2. 69, 10, 2, 15, 30
  3. pivot = begin
  4. left = begin+1
  5. right=end
  6. 69(p), 10(L), 2, 15, 30(R)
  7. 왼쪽에서 오른쪽으로 찾아가면서 오른쪽으로 가야될(피봇보다 큰) 값을 찾아서 보내야됨.
  8. 10, 2, 15, 30 모두 69보다 작은얘들. L이 30까지 갔다. 즉, L이 R로 됐다. 만나있는 상황.
  9. 검색이 완료된 상황이므로 스왑을 하게 된다. R과 피봇을 스왑해준다. 그러면 69와 30이 바뀌어서 69가 맨 끝에 있게 된다.
  10. begin = 0 , end = 4일 때, qsort(0, 3), qsort(5,4)가 나오는데 5,4는 처음 조건부터 갈리게 된다.
  11. qsort(0,3)을 해주면 L과 R이 만나면서 30과 15를 스왑해줌. R포인터와 피봇 위치를 바꿔줌

● 선입선출

● front, rear, dequeue(삭제), enqueue(삽입)

● front 변수. 직전에 빼낸 놈의 위치. -1. 스택의 top 또한 -1로 했던 것처럼.

● front를 증가시켜서 dequeue를 해주는 것.

● front와 rear가 같을 때 공백상태.

● LinkedList는 너무나 종합선물.

● Queue queue2 = new LinkedList();

pdf 305 마이쮸 문제

no, count, total로 3가지 변수가 필요하다.

새로운 사람은 그냥 증가하기만 하면 됨.

  1. 큐의 front에서 dequeue
  2. dequeue된 사람이 가져갈 마이쮸 처리(조건 넣기)
  3. 마이쮸를 가져가고 다시 enqueue
  4. 새로운 다음 사람을 enqueue

큐에서 poll과 remove차이. poll은 return null, remove는 예외를 던짐

SWEA_1225

큐를 사용하면 쉬운 문제. 어차피 레퍼런스를 주기 때문에 반환을 안 해줘도 되는 걸 생각 못 했다.

import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class Swea_1225 {
	
	// 반환을 안 해줘도 된다. void로 하고 return만 줘도 된다. 왜냐면 어차피 레퍼런스만 주니까.
	public static Queue<Integer> generate8(Queue<Integer> queue) {
		
        // 5까지 증가하는 변수
		int j = 1;
		while (true) {
			int k = queue.poll() - j++;
			if (k <= 0) {
				queue.offer(0);
                // 마찬가지로 return;만 해서 빠져나오도록.
				return queue;
			}
			queue.offer(k);
			//여기에서 그냥 j%5+1로 줘서 큰 수로 나눠도 1, 2, 3, 4, 5로 나오도록 할 수 있다.
			if (j == 6) {
				j = 1;
			}
		}
	}

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
        
		Queue<Integer> queue = new LinkedList<Integer>();
        
		for (int t = 0; t < 10; t++) {
               int tcase = sc.nextInt();
            for (int i = 0; i < 8; i++) {
                queue.offer(sc.nextInt());
            }

            System.out.print("#" + tcase + " ");
            // 여기서도 레퍼런스라 따로 넣어줄 필요 없음. generate8(queue); 만 써줘도 됨.
            queue = generate8(queue);

            for (int i = 0; i < 8; i++) {
                System.out.print(queue.poll() + " ");
            }
		System.out.println();
		}
	}
    
}

Bufferedreader a = new Bufferedreader()를 쓸 때 input받을 때, split(“ “)으로 띄어쓰기 구분

  • 배열을 이용한 선형 큐. 3번 인큐, 3번 디큐, 그리고 인큐를 하면 공백상태임에도 불구하고 포화상태라고 나온다.
  • 이걸 고치려면 디큐할 때마다 하나씩 계속 땡겨야된다.
  • 0, 1, 2, 0, 1, 2, … 안으로 계속 움직이게 하려면 (r+1)%size로 해주면 된다.
  • 포화상태와 공백상태를 구분해야 함.
    • front 공간을 안 쓰고 front와 rear를 만났을 때 포화상태를 식별하면 된다.

제대로 된 큐

  1. front = rear = 0;
  2. front+1, rear+1을 %size해서 연산해야 된다.
  3. 포화상태 판단조건 변경(0 인덱스 공간은 사용하지 않음)

우선순위 큐.

우선순위가 있는 큐.

리스트

링크드 리스트의 구현은 알아둬야 한다.

  • 데이터 필드와 링크필드
  • 링크 필드에 의해서 순서가 유지됨.
  • head - 시작점, tail도 따로 둘 수 있음(유지?)
    • tail이 있으면 좋은 이유?
      • 마지막 노드를 쉽게 찾을 수 있음
  • 단순 연결 리스트
    • 다음 노드의 링크가 null일 때까지
    • 삭제할 때 뒤에 있는 노드를 끊어주고 앞에 있는 건 끊지 않아 줄 수 있다. 하지만 끊지 않으면 앞에 노드가 물고있기 때문에 그 노드마저 삭제했을 때 물고있는 상태로 남아서 gc가 처리하지 못 한다.
    • 삭제할 때 그 삭제하려는 노드의 이전노드를 null로 하면 끊김.
    • 인접 리스트
      • A -> B, A -> C, A -> D, B -> E 와 같은 간선, 만들어보면 그래프로 들어올 때 인접 행렬로 해도 되지만 데이터가 너무 커진다. 예를 들어, 10000개라면 10000*10000으로 해야된다. 그리고 X표 된 건 쓰지도 않는데 하나하나 모두 찾아야 되고. 그래서 인접 리스트로 푸는 게 좋다.
      • A의 칸에 head에 있는 얘를 바꿔주면서 첫 번째 노드로 계속 추가하면 된다. 예를 들어, A -> B, A -> C, A -> D의 경우 A - D - C - B로 연결된다. 하지만 이게 C와 B가 연결되어있는 게 아니고 A가 각자 연결되어있다고 생각해야 된다. 그리고 탐색만 잘하면 된다. 첫 번째로만 집어넣어주면 된다. 순서를 생각하지 않아도 된다. A의 정점에서 갈 수 있는 인접리스트라고 얘기해야 된다.
  • 이중 연결 리스트

++

wsalgo07

링크드 리스트로 스택을 만드려면?

  • push : 첫 번째 노드에 계속 추가하는 방식
  • pop : 삭제 또한 첫 번째 노드에서 삭제하는 방식.

링크드 리스트로 큐를 만드려면?

  • enqueue : 마지막 노드 추가
  • dequeue : 첫 노드 삭제

` return top == null;`처럼 바로 true와 false가 나오게 할 수 있다.

hwalgo07도 직접 구현해서 풀기. 1228

병합정렬

  1. 집합을 둘로 나눈다. (분할)
  2. 왼쪽집합을 정렬한다
  3. 오른쪽집합을 정렬한다
  4. 두 집합을 합침 (병합)

트리

  • 비선형 자료구조
  • 1:다 관계를 가지고 있음
    • 1이 root
  • 리프 노드 : 마지막 노드
  • 이진트리
    • 두 개로만 나눠짐
  • 높이
    • ROOT가 0.
  • 완전 이진 트리
    • 포화 이진 트리의 모습을 지키면서 순서대로 맞춤. (왼쪽부터 붙인다)
    • 왼쪽이 없으면 오른쪽도 없다? 완전 이진트리이기 때문에?
  • 순회 방법 3가지 -> 모두 DFS.
    • 전위(VLR), 중위(LVR), 후위(LRV)
    • ROOT를 언제 탐색하느냐에 따라 달라짐. 왼쪽과 오른쪽의 순서는 크게 신경 안 써도 된다.
      • 우리가 생각하는 전위 순회는 바로 루트를 탐색
    • 재귀적으로 호출하면 된다.
    • 자식이 없으면 탐색을 안 해도 되니까.
  • 2개로 나눠지는 규칙성을 이용해서 완전 이진 트리를 배열로 구현하면 인덱스를 관리하기 편하지만 편향 이진트리를 배열로 쓰게되면 1, 2, 4, 8로 진행되기 때문에 비효율적이게 된다.
  • 자기 인덱스의 2를 나누면 부모 인덱스가 나옴.

수식트리

  • 피 연산자가 두 개라서 이진트리로 이용할 수 있다.

이진탐색트리

  • 트리를 모두 순회할 필요가 없고 범위가 점점 좁아진다. 임시 노드로 잡아서 temp를 계속 바꿔주면서 내려가면 된다.
  • 삭제 : 자식이 없는 경우, 1개 있는 경우, 2개 있는 경우 다 따져야 된다. 조정하면서 다 땡겨야함…

중위순회 시간 남으면 문제 풀기 1231번문제.

++

SWEA_1233

노드

  • 연산자
    • 자식이 연산자, 연산자인 경우
    • 자식이 연산자, 숫자인 경우
    • 자식이 숫자이면, 다른 쪽은 무조건 숫자. 왜냐면 완전 이진 트리니까.
    • 즉, 왼쪽 자식 노드가 숫자일 때 오른쪽 자식 노드 숫자인지 체크.
  • 숫자 - 자식 노드의 유무.

그래프

정점(Vertex)과 간선(Edge)의 집합.

비선형 자료구조. (한 줄로 줄 세울 수 없다)

V개의 정점을 가지는 최대 간선 수 : V(V-1)/2 –> 양방향 간선이 아니기 때문에 /2를 해줌.

인접 행렬

두 정점을 연결하는 간선의 유무를 행렬로 표현

V*V 행렬로 만듦. 행렬에서 ROW를 진출로 봐라. col을 진입으로.

만약 가중치가 없다면 boolean 형으로 만드는 게 좋다. 왜냐면 메모리적 측면의 이유가 크다. int형을 부르면 3()

간선이 많을 때 쓰임.

간선이 적고 정점이 많을 때 쓰인다.

인접 리스트 : 해당 정점으로 나가는 간선의 정보를 저장.

간선들의 목록을 갖고 있는 게 중요하다.

행을 인접 리스트로 생각하자.

대칭이 되는 성질이 있다.

무향과 유향을 잘 생각해봐야 한다.

int형 배열이면 가중치가 들어간다고 생각.

유향 그래프는 대칭이 아님.

인접 리스트

간선들의 정보만 구성되어있어서 공간적인 낭비는 없지만 따로 정렬하지 않는 이상 순차탐색을 해야 한다.

각 정점에 대한 인접 정점들을 순차적으로 표현

하나의 정점에 대한 인접 정점들을 각각 노드로 하는 연결 리스트로 저장

새로운 노드를 추가할 때 마지막에 노드를 추가하는 알고리즘을 쓰지 말고 앞으로 추가하는알고리즘을 쓰자. 마지막 노드를 추가하려면 맨 뒤로 가서 추가를 시켜줘야 한다.

  • 인접 리스트는 순차탐색을 해서 모두 다 검색해야하지만 인접리스트를 연결리스트로 저장한다면 해당하는 정점에 대해서 간선이 있기때문에 더 빠르게 간선의 정보를 찾을 수 있다.
  • 새로 생성할 때 head에 집어넣기

무향 그래프일 경우

  • 인접 리스트에 양방향으로 같이 추가를 해줘야 한다.

인접행렬로 DFS, BFS는 기본…

친구관계문제

  1. 카운트를 줘서 풀면 된다.
  2. depth count에 관한 배열을 만들어서 수를 넣어주는데 같은 depth일 때 비교하면 된다.

같은 depth마다…

상호배타 집합

Union find -> 크루스칼 알고리즘으로 풂.

-1은 자기 자신이 유일하다는 소리. 자기 혼자 root.

트리처럼 관리하는 건 아니지만 부모로 가면 root를 찾을 수 있다?

a가 b의 부모라면 부모로 주고 싶은 얘들의 인덱스를 넣어주면 된다.

parent라는 배열에 [-1, 0, -1, -1] 각각 abcd(0123)

[-1, 0, -1, 2] -> [-1, 0, 1, 2]

이게 사이클을 판단하는 요인이 된다.

최소 신장 트리를 만들려면 사이클이 있으면 안됨.

합치기 전에 찾아서 판별. union find 알고리즘

Union(d, f)할 때 e의 루트를 c의 자식으로 삼음.

찾은 부모 f로 다시 올 때 자기의 부모로 바꾼다.

pdf 312 부모를 모두 root로 바꿈.

정형화된 코드로 짜면 된다.

-1로 초기화하는 이유는 애초에 0인덱스로 초기화되기 때문에.

자기 자신이 루트면 자기 인덱스를 가져옴.

간선(Edge)클래스를 만들어서 쓰자

어차피 최소 비용으로 sort하기 때문에 바로 끊어주면 된다.

  1. 비용이 최소인 간선을 이용해서 연결해나간다.
  2. 사이클 형성되면 안함.
  3. 간선정보를 안주면 간선의 비용을 구해야됨…

Union Find 자료구조

Find

어떤 원소가 어떤 집합에 포함되는지 아닌지를 판별

인덱스만 따라가면 된다.

Root가 같으면 같은 집합에 있다.

Union

서로 다른 두 개의 집합을 하나의 집합으로 합병(집합에 포함되면 합병하고 포함되지 않으면 안 하고)

링크드리스트로 원소를 넣어서 찾아볼 수 있지만 O(n)이라는 시간이 걸린다. 상향식 트리 구조로 구현하면 효율적이다.

같은 집합으로 바꿔주려면 그냥 그 root 노드를 다른 집합의 Root의 자식으로 두면 된다.

높이가 낮은 집합을 높이가 높은 집합의 자식으로 두는 게 좋다. 그러면 합칠 때 높이가 같아질 수 있으니까.

비트 연산자

두 번째 비트가 켜져있는지 궁금하면 0010을 & 연산하면 된다.

는 비트를 합치는 연산

쉬프트 연산은 내가 원하는 자리에 1을 놓기위한 방법

쉬프트 두 개짜리는 부호비트로 채운다~

부동 소수점

123.625는 많은 방식으로 표현이 가능하다.

12.3625

1.23624*10^2으로 고정. 소수점의 위치를 왼쪽의 가장 유요한 숫자 다음으로 고정시키고 밑수의 지수승으로 표현한다. 첫 번째 숫자 뒤에 .이 있다고 생각한다.

소수 아래부분만 기억한다.

두 개를 기억하지 않게 되는데

  1. 소수점의 위치
  2. 1.을 버림.

소수점의 위치까지 기억을 해야한다.

IEEE 754 포맷에 해당 내용이 있다. 32비트를 쓴다고 하면 맨 앞에 sign비트, 지수부 8, 가수부 23비트이다.

64비트일경우, 1, 11, 52 이다.

가수부분에 1. 뒤에 부분을 기억한다. 이진화된 값으로 들어간다.

sign비트를 이용하지 않고도 지수를 기억하기 위한방법

  1. 지수부를 8비트라고 생각하면 256이 가능하다.
  2. 그러면 128을 기준으로 10^-3이면 125, 10^5이면 133으로 기억을 한다.
  3. 즉, 중간 값을 기준으로 음수 지수와 양수 지수를 판단한다.

데이터 타입 자체를 실수 연산으로 하면 좋지 않다.

이래서 웬만하면 실수 연산은 하지 않는 게 좋다.

Backtracking

답을 찾았다면 Root로 return

가지치기(Prunning)하기 전에 완전탐색으로 잘 되나부터 확인. (등호같은 거 조심.)

  • 대각선이 안 된다.
  • 십자가가 안 된다.

메모이제이션 테이블을 어떻게 만들지 생각.

N queen

일차원 배열만 생각해도 됨. 4*4라면 4개의 배열. 열만 생각해도 된다.

대각선의 규칙성으로 찾겠다는 소리

행 차이와 열 차이로 생각해서 푼다. [1, 3, ?, ?] 이면이 규칙을 행 차이는 1이고 열 차이가 2므로 대각선에 포함안됨. 대각선에 걸리지 않는다. 이런 방식으로 계속 체크를 해보면 된다. 바꿀 때마다 열 체크, 대각선 체크를 해주면 된다. 행 체크는 배열의 인덱스로 다르니까 상관 없다.

다 했는데 안 되면 백해서 값을 바꾸고 체크한다.

[1, 4, 2, …] 다 안됨. 1열이 안 되는 거였음. 그럼 2열로 감

N-1번 비교하면 된다. 배열은 총 3개로 체크하면 된다. 열을 체크할 배열. 우상향 - 각 좌표의 합이 똑같다(2~8), 우 하향배열 - 각 좌표의 빼기가 같다.(-3~3)

이진탐색트리

리프 노드의 삭제는 쉽다.

리프 노드인 13을 삭제한다면 그냥 삭제가 가능하다. 그러나 차수가 1인 것, 2인 걸 없앨 때는 어렵다.

차수가 2일 때는 자식중에 하나가 위로 올라와야 함. 왼쪽 서브트리보다는 크면서 오른쪽 서브트리보다는 작은 노드를 선택해야 함.

완전 이진 트리로 구성. 최대 힙이면 루트 노드가 가장 큰 값, 최소 힙이라면 가장 작음. 우선순위 큐에서 쓰인다. default는 최소 힙. 오름차순. 최대힙일 때, 리프 노드랑 바꾸면서 가장 큰 루트노드를 뽑아서 배열에 넣는다. 최대이게 바로 힙 정렬.

알고리즘

가중치가 없는 간선일 경우, BFS로 푸는 게 가장 빠르다.

Prim 알고리즘

  1. 임의의 정점에서 시작한다.
  2. 그 임의의 정점과 인접하는 정점들 중에 최소 비용의 간선이 존재하는 정점을 선택한다.
    1. 전에 있던 정점들을 포함해서 최소 비용의 간선을 찾는 것이다.
    2. 당연히 이미 다 연결된 대상은 고려하지 않는다.
    3. 경로 형태의 정점을 선택하는 것이 아니다.
    4. 정점들이 연결되어 있기만 하면 된다.
  3. 모든 정점이 선택될 때까지 1, 2번을 반복한다.

pdf.

0번 정점 기준으로는 min이 7인데 1번 정점 기준으로는 3이 최소.

  1. 방문했던 정점을 기준으로 방문하지 않은 정점을

연결되지 않은 정점은 넣을 때 바로 살펴보기에 사이클링을 살펴볼 필요가 없다.

브루트포스와 좀 더 개선된 코드, 두 개의 코드로 표현할 수 있다.

Brute Force(브루트 포스)

  • 모든 경우의 수를 다 시도해보는 알고리즘
  • 시간복잡도는 O(전체 경우의 수 * 하나를 시도하는데 걸리는 시간)

재귀함수를 이용한 브루트포스

void go(int x){
     if(정답을 찾음){
        정답을 찾은 처리;
     }
     if(불가능한 경우) return;
     for(가능한 모든 다음 정점 y){
        if(check[y] == false){
            check[y] = true;
            go(y);
        }
     }
}
check[시작] = true;
go(시작);

선택된 정점의 간선들 가중치를 계속해서 더하면 그게 최소가 되는 것

다익스트라 알고리즘

단일 출발점에서 단일 도착점으로 갈 때.

가중치가 양수일 때(1 이상)만 가능하다. 음의 가중치가 있는 선이 있다면 간선을 거칠 수록 값이 작아질 수 있기때문에 안 된다.

최단 경로라서 Path이기 때문에 누적된 간선 비용이 된다.

중간 경유지를 모두 반영한다.

미지의 큰 값을 줘놓고 시작. 왜냐면 나중에 경로에 따라서 갈 수도 있으니까.

a출발점에서 각 정점들을 도착점 개념으로 본다.

a 출발점에서 각 정점에 오는 최소 비용. a에서 b, a에서 c, a에서 d…

방문 했다면 방문한 얘들을 제외한 얘들을 다시 보기.

유리한 쪽을 업데이트 해줌.

a - b - c, a - b - d, a - b - f

a - c - d, a - c - e, a - c - f

첫 출발만 경유지와 출발점이 같은 선상에 있으니까 그런거고

나머지 얘들은 모두 경유지 개념.

0 - 1 - 4로 가는 것과 0 - 4로 가는 걸 비교해서 더 작으면 갱신해줌.

문자열

매우 어렵다. 자기 것화 하기가 어렵다. 시험에 잘 안나오기 때문에 현 상황에서는 깊게 보지 않아도 된다. 집중적으로 보완할 생각보다 다른 문제를 먼저 풀어라. 다양하게 풀어보려 하지말고 아니다 싶으면 바로 바꾸기.

랜덤하게 한 문제씩… 난해한 거 말고… 월말은 IM 2문제… 2시간.

동적 생성정도는 할 수 있게… 프론트 form 처리. 박스에 입력된 값들. 영화 티켓같이

테케 데이터 inputbox. 브라우저 dom처리, 이벤트 처리.

해쉬함수

충돌나지 않게 좋은 해쉬함수가 좋다.

라빈카프 알고리즘이 해쉬의 원리를 이용한다. 문자를 하나씩 비교하는 게 아니라 내가 찾는 문자열의 해쉬코드를 미리 만들어놓고 해쉬코드상으로 비교한다. 그래서 하나씩 비교하는 일이 없어진다.

10을 곱하고 앞에 값을 제거하고, 뒤를 붙여서 해쉬값을 쉽게 구할 수 있다.

KMP

패턴이 반복되는 문자열이 있을 경우 이 알고리즘을 쓰기 좋다.

실패가 발생한 위치부터 반복적인 패턴을 넘기기 위해서.

탐색하는데 실패했을 경우, 원본 문자열의 어디서부터 검색할지?

접미사 배열(SA)

banana -> N개의 길이를 갖는 문자열은 N개 만큼 나온다. 브루트포스로 하나하나 다 비교하게 되면 N^logN으로 많이 느리다.

입력 데이터의 범위를 보고…

접미사들은 반복적인 부분을 보고…

번호를 가지고 문자열을 대신한다.

접두사로 바로 앞에 일치하는 얘가 있는지 없는지.

접미사 배열을 만들어서 소팅하는 것.

앞에 몇 개가 일치하는지 만족하는 최장 접두사. 부분문자열을 만들 수 있다…

LCP

SWEA_1257

food라는 문자열이 들어왔을 때 접미어를 추출하면

food, ood, od, d가 나온다. 여기서 배열을 정렬한 후, 다시 접두어를 추출하면, d, food, foo, fo, f, od, o, ood, oo, o가 나온다.

그냥 쓰고 카운트를 늘려주면 될듯. 1, 4, 2, 3개니까 만약에 찾는 개수가 8개라면 8개에서 줄여가면서 0이 될 때까지 줄인다. 그러다가 LCP가 숫자가 나올 때 한 번 더 추가해줘서 하면 될 것 같다.

DP

완전 검색을 하는데

PDF 439

1-1, 2-1, 3-2, 4-3, 5-5, 6-8. 즉, 1, 1, 2, 3, 5, 8 피보나치 수열이다.

메모이제이션이 필요한지 상태공간트리를 그려보면서 생각해보자.

피보나치 수열에서 메모이제이션을 생각한다면 memo[]라는 일차원 배열을 만들어놓고 모두 -1로 일단 채워 놓는다. 그리고 -1일 때는 그 값을 채워놓도록 알고리즘을 짜면 된다.

그 다음 문제에 적용해보기. 가장 작은 단위의 항부터 구한다.

재귀 탈출 조건처럼 dp로 피보나치를 풀려면 0과 1의 인덱스는 값이 무조건 있어야 함.

dp는 역으로 생각해야한다. 작은 단위의 성질을 찾아야 한다.

가장 작은 단위부터 성질을 이용해서 현 시점의 최적해를 구한다.

분할 정복과는 dp와 다르다. 분할 정복은 같은 부분 문제가 나타날 경우 다시 계산한다. 재사용하지 않음.

PDF 456

피보나치 수열이 된다. 노파, 노노파, 노노노파파, 의 형식으로 계속 올라가면 된다.

노란색이면 노와 파로 나눠지고 파면 무조건 노이다. 이런 방식도 가능하다.

PDF 472

동전 거스름 돈 구하기. 8원을 거슬러주는데 1, 4, 6원이 있다면?

1원을 썼을 때 7원의 최적해, 4원을 썼을 때 4원의 최적해가 있다. 그러나 이렇게 하게 되면 중복적힌 호출이 많이 일어난다. 이 문제는 DP에 메모이제이션을 해도 되지만 많은 돈을 거슬러주게 된다면 메모리가 많이 필요하게 된다.

dp 접근 상향식 : 1원에 대한 최적해, 2원에 대한 최적해, 3원에 대한 최적해… 8까지 다 찾아본다.

PDF 478

nCr = n-1Cr-1 + n-1Cr

2차원 배열로 한다.

PS 팁

  • 문제를 풀기보다 생각하는 시간을 많이 갖자
  • 종이로 풀자
  • 누가 많은 문제를 풀어봤느냐가 관건이다
  • 3시간 넘게 걸린다면 그냥 해답을 보자. 왜냐면 그 시간이 지나서 풀면 풀어도 최적화된 코드가 아닐테니까.
  • 된다면 2~3 가지 방법을 생각해보자
  • 자기 생각에 갇히지 말고 잘 안 될 때는 빠르게 버릴 줄도 알자

사람들과의 관계성, 인테크.

  • 속도 문제가 크다
  • 평균적으로 문제를 풀 때, 시간이 얼마나 걸리는 지 파악.
  • 어디서 오래걸렸는지 파악.
  • 자기 생각에 너무 갇히지 말고 안 되면 다른 방법을 모색하자.
  • 3시간이 넘는다면 해답을 보자.

순열과 조합

순열은 완전탐색으로도 풀 수 있기 때문에 잘 나오진 않는다.

조합은 많이 나오는데 next Permutation이라는 알고리즘으로 풀면 빠르다.

BufferedReader vs Scanner

둘 다 동시에 썼더니 스트림에 오류가 생겼는지 입력에 이상이 생긴다. 쓸 거면 하나만 쓰자.

시간복잡도 계산

문제를 풀 때 시간부터 먼저 보고 BufferedReader를 쓸지, Scanner를 쓸지 판단. 당연히 편한 건 Scanner다.

처음에 N 사이즈부터 보고 판단해야된다고 말씀하심.. 교수님도 그랬음.

1초에 1억번 정도 연산

6719 예시

100개의

n!은 조심해야됨. n>12만 되어도 5억정도가 됨

  • 행렬을 풀 때 x와 y가 아닌 r과 c를 변수로 두자.
  • 메소드를 만들 때 맞다고 확신하지 말고 디버깅으로 System.out.println();을 한 번 찍어보자.
  • 입력 받자마자 먼저 확인을 해본다.
  • 일단 소팅하는 걸 생각해보자.
  • 어떤 생각을 하기 전에 최대 N값을 넣어 최악의 경우를 보자. 시간복잡도부터 생각하자.
  • length와 length()는 다르다. length()는 함수이기 때문에 부를 때마다 호출해야해서 변수에 넣어 넣고 쓰는 게 좋다.
  • dfs는 시간이 오래 걸린다. bfs는 완전탐색이나 가중치가 없는 그래프, 등등 훨씬 더 많이 쓰인다
  • clone함수는 쓰지말자. 2차원 배열일 경우 아예 복사해버리는 함수를 하나 만들자.
  • 사방탐색을 할 때는 dr, dc로 미리 만들어놓자. 또는 %4로 처리하는 경우도 있다. 방향을 숫자로 해서.
  • pro는 ArrayList나 Queue가 안되니까 아예 지금부터 안 쓰는 연습을 하면 좋다. 단순히 list로만 구현하는 연습. max같은 것들도. 속도의 빠름은 Array > ArrayList > LinkedList 순이다. 최대한 컬렉션 사용을 자제하자. 속도가 더 안 나오니까.
  • search라는 함수를 만들어서 아예 쭉 돌도록. cctv하나당 방향 탐색하고 바로 다른 cctv로 가도록.
  • long형과 int형의 범위를 잘 알고 있어야 한다. int형이 21억이니까 그 범위를 넘으면 Long형을 써야한다는 것도 감안

학교에서 배운 알고리즘들

<알고리즘 공부=""> 중간고사 복습 ////////////////////////////////////////////////////// *Bubble Sort 이웃하는 숫자를 비교하여 작은 수를 앞쪽으로 이동시키는 과정을 반복 (마치 '거품'처럼 위로 올라감) -sudo code(크기가 n인 배열 A) for pass = 1 to n-1 // 총 n-1번을 비교해야 된다. (A[0]과 A[1], A[1]과 A[2] ...) for i= 1 to n-pass // 오름차순이면 이미 정렬 한 뒷 쪽 얘들은 정렬하지 않아도 되니까. if (A[i-1] > A[i])// 왼쪽의 원소가 오른쪽의 원소보다 크면 A[i-1] ↔ A[i]// 서로 자리를 바꿈 return 배열A 총 비교 횟수는 : (n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) *Selection sort 배열 전체에서 최솟값을 '선택'하여 배열의 0번 원소와 자리를 바꾼다. 다음에 정렬할 때는 0번 원소를 제외한 나머지 원소에서 최솟값을 선택하여, 배열의 1번 원소와 자리를 바꾼다. -sudo code(크기가 n인 배열 A) for i= 0 to n-2{ // n-2까지 해야 밑의 for문에서 n-1까지 찾는다. min = i // min에 0값을 대입시켜서 다음 값과 계속 비교하게 함. for j = i+1 to n-1{ // A[i]~A[n-1]에서 최솟값을 찾음 if (A[j] < A[min]) //min값이 그 다음 값보다 크면 min = j //min을 그 다음 값으로 바꿔줌. } A[i] ↔ A[min] // min의 원소 값과 처음에 했던 i값과 바꿈. } return 배열A 총 비교 횟수는 : (n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) *Insertion sort 배열을 정렬된 부분(앞 부분)과 정렬 안 된 부분 (뒷 부분)으로 나눈다. (맨 처음에 첫 번째 원소는 정렬 되어있고 두 번째 원소부터 정렬 안 되어있다고 생각하면 쉽다.) 정렬 안 된 부분의 가장 왼쪽 원소를 정렬된 부분의 가장 오른쪽 원소와 비교하는데 정렬 안 된 부분의 가장 왼쪽 원소가 정렬된 부분의 가장 오른쪽 원소보다 작으면 정렬 된 부분의 가장 오른쪽 원소가 오른쪽으로 한 칸 씩 옮겨진다. 그러다가 그러지 않는 곳이 있으면 그곳에 정렬 안 된 부분의 가장 왼쪽 원소를 넣는다. -sudo code(크기가 n인 배열 A) for i=1 to n-1 { 얘도 bubble sort와 마찬가지로 정렬된 부분과 정렬 안 된 부분 기준이므로. n-1번 CurrentElement= A[i] // 정렬 안 된 부분의 가장 왼쪽 원소를 element에 따로 넣음 j ← i-1// 정렬된 부분의 가장 오른쪽 원소 인덱스를 j에 넣음 while (j >= 0) and (A[j] > CurrentElement) { // j가 0보다 작으면 마이너스 인덱스라 안 되고 거기까지 비교해야 되니까, 정렬된 부분의 가장 오른쪽 원소가 정렬 안 된 부분의 가장 왼쪽 원소보다 크다면 A[j+1] = A[j] // 바로 오른쪽에 정렬된 부분의 가장 오른쪽 원소값을 대입시켜서 오른쪽으로 옮겨버림 j ← j-1 //그리고나서 j를 하나 깎고 다시 while문으로. } A [j+1] ← CurrentElement // A[j]가 더 작으니까 그 다음 자리가 비었을 테니 거기다가 element를 넣어줌. } return A 총 비교 횟수는 : (n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) *Shell sort bubble sort와 insertion sort의 단점을 보완함. 배열 뒷 부분의 작은 숫자를 앞 부분으로 이동시키고 동시에 앞 부분의 큰 숫자는 뒷 부분으로 이동시키고. 간격이 1일 때는 삽입 정렬을 수행한다. 간격을 설정하고 그 다음 간격은 더 작게 설정한다. 그리고 마지막에는 간격을 1로 놓고 수행한다. 쉘 정렬의 수행 속도는 간격 선정에 따라 좌우 된다. for each gap h = [ h0> h1> ... > hk=1] //갭을 지정한다. for문을 써서 /2를 해 계속 낮춰줄 수 있다. 마지막은 1이라 삽입정렬이 되도록. for i = h to n-1{ // //h부터 n-1까지. 밑에 j-h가 있어서 h이면 0이되기 때문에 간격만큼 비교하는 게 된다. CurrentElement = A[i] ; //차례대로 current에 넣는다. 바꿔주려고. j = i ; // 위와 동일한 인덱스를 주기 위해서 i를 j에 넣음. while ( j >= h) and (A[j-h] > CurrentElement) { // j값이 바뀌지 않는 이상 왼쪽은 무조건 성립하고 오른쪽이 비교하는 것! 간격만큼의 값보다 큰지 작은지. 만약 왼쪽에 있는 원소가 더 크면 A[j] = A[j-h]; //오른쪽 원소에 왼쪽 원소를 넣고 j ←j-h; //오른쪽 원소 인덱스를 왼쪽 원소 인덱스로 바꿔준다. 얘가 간격만큼 빠지니까 그 다음도 간격만큼 빠지는 값을 봐야 된다. j >= h가 만족하고 A[j-h] > current면 또 바꿔줄 수 있다. } A[j] ←CurrentElement; //그러면 바로 currnet를 A[j]에 넣어줌으로써 완전히 바뀌게 된다. } return A 최악의 경우 시간복잡도 : O(n^2) 최선의 경우 시간복잡도 : O(nlogn) 쉘 정렬의 시간복잡도는 아직 풀리지 않은 문제다. 쉘 정렬은 입력 크키가 크지 않은 경웨 매우 좋은 성능을 보인다. 보통 Embedded 시스템에서 주로 사용한다. *Heap sort 힙 조건(각 노드의 값이 자식 노드의 값보다 커야/ 작아야 함)을 만족하는 완전 이진 트리(트리의 원소를 왼쪽에서 오른쪽으로 하나씩 빠짐없이 채워나간 형태). 노드들을 빈 공간없이 배열에 저장한다. 힙의 루트에는 우선순위가 가장 높은 값이 저장됨. (가장 큰 값 또는 가장 작은 값) n개의 노드를 가진 힙이라면 힙 높이는 log_2⁡n이 된다. -힙에서 부모 노드와 자식 노드의 관계 A[0]은 비우고 A[1] ~ A[n]까지 힙 노드들을 트리의 층별로 좌에서 우로 배열에 저장한다. A[i]의 부모노드 = A[i/2] A[i]의 왼쪽 자식 노드 = A[2i] A[i]의 오른쪽 자식 노드 = A[2i + 1] 오름차순 정렬 시 힙의 루트에는 가장 큰 수가 저장된다. 그리고 루트의 숫자를 힙의 가장 마지막 노드에 있는 숫자와 바꾼다. 바꾼 마지막 노드의 숫자가 힙 조건에 위배되면 힙 조건을 만족시켜야 하니 밑의 자식들 중에서 큰 자식과 바꾸고(DownHeap()), 루트를 고정시키기 위해 힙 크기를 1개 줄이는 식으로 해서 반복한다. . - 배열A의숫자에대해서힙자료구조를만듦 heapSize= n // heapSize: 힙크기 for i= 1to n-1 A[1] ↔ A[heapSize]// 루트와힙의마지막노드교환 heapSize= heapSize-1// 힙의크기를1 감소 DownHeap()// 위배된힙조건을만족시킴 return 배열A /////////////// #include #include #pragma warning(disable:4996) void DownHeap(int *arr, int i) { int j, n, temp, flag = 1; n = arr[0]; while ((i * 2 <= n) && (flag == 1)) { j = i * 2; //왼쪽 자식의 인덱스 if ((j + 1 <= n) && (arr[j]<arr[j + 1])) j = j + 1; //오른쪽 자식의 인덱스 if (arr[j] < arr[i]) flag = 0; else { //자식노드의 temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i = j; //얘가 DownHeapap을 계속 하게 해줄 수 있다. 비교해서 자식이 더 큰지 작은지. 확인해주는 역할 } } } void BuildHeap(int *arr) { //빅오의 n으로 볼 수 있다. int i, n; n = arr[0]; //그대로 main함수에 있던 n. 즉, size가 들어간다. for (i = n / 2; i >= 1; i--) //n의 반 만큼 횟수를 지정. 이유는 노드가 그 만큼이기 때문에 9개면 4번 실행하면 되니까. DownHeap(arr, i); //4,3,2,1 순서로 실행해준다. 즉, 완전 이진트리인지 확인하는 거. } int main(void) { int n, A[20], i, j, last, temp; printf("array size : "); scanf("%d", &n); printf("\ninput the array : "); for (i = 1; i <= n; i++) scanf("%d", &A[i]); A[0] = n; //아예 사이즈를 A[0]에다 넣고있음. BuildHeap(A); //BuildHeap함수 실행 printf("\nmax heap\n"); for (i = 1; i <= n; i++) printf(" %d", A[i]); while (A[0] > 1) { last = A[0]; //사이즈를 last에 넣음 temp = A[1]; //임시로 A[1]값을 temp에 넣어준다. A[1] = A[last]; //last를 인덱스로 줘서 원소값을 바꿔준다. A[last] = temp; //바구는 중. A[0]--; //그리고 heap을 하나 줄여주기 위함. 9에서 8으로... DownHeap(A, 1); //8,1을 넣어서 실행시킴. 그리고 나오면 다시 반복. printf("\n\nmiddle value result\n"); for (i = 1; i <= n; i++) printf(" %d", A[i]); } printf("\n ** array result ** \n"); for (i = 1; i <= n; i++) printf(" %d", A[i]); getchar(); getchar(); getchar(); getchar(); getchar(); return 0; } Divide - and – Conquer Algorithm 주어진 문제의 입력을 분할하여 문제를 (정복)해결하는 방식. 분할한 입력에 대하여 동일한 알고리즘을 적용하여 해를 계산하며, 이들의 해를 취합하여 원래의 해를 얻음. 분할된 입력에 대한 문제 : 부분 문제 // 이 때의 해 : 부분해 부분 문제는 더 이상 분할할 수 없을 때까지 분할함. 크기가 n인 입력을 3개로 분할한 경우(각 분할된 부분 문제의 크기) -> n/3 입력 크기가 1인 경우, 더 이상 분할하지 않음. 입력 크키가 n일 때 총 몇 번을 분할하면 더 이상 분할할 수 없는 크기인 1이 될까? -> 총 분할한 횟수를 k, 1/2씩 분할된다고 하면... 1번 분할했을 때 n/2, n/2^2, n/2^3...n/2^k가 된다. n/2^k가 = 1일 때 더 이상 분할할 수 없다. 그러므로 k = 〖log〗_2^n이다. 문제가2개로분할되는알고리즘–부분 문제의 크기가1/2로감소하고,1개의 부분 문제는고려할필요없음: 이진탐색(1.2절) –부분 문제의 크기가 일정하지 않은 크기로 감소: 퀵정렬(3.2절) –부분 문제의 크기가 일정하지 않은 크기로 감소,1개의부분문제는고려할필요가없음: 선택문제알고리즘(3.3절) •부분 문제의 크기가 1, 2개씩감소하는알고리즘: 삽입정렬(6.3절), 피보나치수(3.5절) 등 Merge : 2개의 각각 정렬된 숫자들을 1개의 정렬된 숫자들로 합치는 것. Merge sort 분할 정복 알고리즘 입력을 2개의 부분 문제로 분할, 부분 문제의 크기가 1/2로 감소함. (재귀적으로 나눌 수 없을 때까지 분할) (동일한 문제들로 분할) -> 각각의 부분 문제를 재귀적으로 합병하여 정렬(정복)함 분할 순서를 잘 봐라. 5에서 6갈 때 6이 위에 있다. -pseudo code MergeSort(A,p,q) 입력: A[p]~A[q] // 출력: 정렬된A[p]~A[q] if ( p < q ) { // 배열의 원소의 수가 2개이상이면 //어차피 p에 0이 들어가고 q에 n-1이 들어가기 때문에 저 공식을 써도 됌. k = (p+q)/2 // k; 반으로 나누기 위한 중간 원소의 인덱스 MergeSort(A,p,k) // 앞 부분 재귀호출 MergeSort(A,k+1,q) // 뒷 부분 재귀호출 Merge(A, p, k, q) // A[p]~A[k]와A[k+1]~A[q]를 합병함 } -code #include #pragma warning(disable: 4996) #define MAX 50 void merge(int*, int, int, int); void mergeSort(int*, int, int); int main(void) { int A[MAX], i, n; printf("array size : "); scanf("%d", &n); printf("\ninput the array : "); for (i = 0; i < n; i++) scanf("%d", &A[i]); mergeSort(A, 0, n-1); //mergeSort함수로 감. printf("\n \n"); for (i = 0; i < n; i++) printf("%d ", A[i]); //A배열을 출력함. getchar(); getchar(); getchar(); getchar(); return 0; } void merge(int A[], int low, int mid, int high) { int i, m, k, l; int temp[MAX]; //temp는 A와 크기가 같아야 함. 공간을 따로 둬야된다. 똑같은 게 있어야됌 l = low; //처음부터 확인 i = low; //temp의 처음부터 시작해야되니까 m = mid + 1; //mid의 바로 다음 것부터 시작. (중간 다음부터 비교를 해야되니까.) while ((l <= mid) && (m <= high)) { //맨 처음이 중간을 지났냐?, 중간다음이 맨 끝을 지났냐? if (A[l] <= A[m]) { //인덱스가 아니라 원소다 착각하지말자. 맨 처음과 중간 다음을 비교함. temp[i] = A[l]; //맞으면 천천히 대입하면서 증가시킨다. l++; //증가시키고 다시 돌린다. 대입했으니 그 다음으로. printf("LEFT : temp[%d]=%d\n", i, temp[i]); } else { temp[i] = A[m]; //만약 m쪽이 더 작으면 그걸 다음 원소에 넣어줘야지. m++; //증가시켜주고. printf("RIGHT : temp[%d]=%d\n", i, temp[i]); } i++; //다음 원소 바꾸거나 넣어주려고. } if (l > mid) { //왼쪽 부분은 다 했으니까 m이 커졌던 부분에서부터 오른쪽 값을 temp에 복사 for (k = m; k <= high; k++) { //mid+1을 넣고 high까지 temp[i] = A[k]; i++; printf("RIGHT : temp[%d]=%d\n", k, temp[i-1]); } } else { //오른쪽 부분이 완료됐으니 커진 l에서부터 왼쪽 값을 temp에 복사 for (k = l; k <= mid; k++) { // l을 증가시키면서 그 원소값을 넣어줌. temp[i] = A[k]; i++; printf("LEFT : temp[%d]=%d\n", high, temp[i-1]); } } printf("RESULT : "); for (k = low; k <= high; k++) { A[k] = temp[k]; //temp에다 넣어줬던 걸 다시 A[]에 넣는 과정. printf("%d ", A[k]); //이제 merge가 하나 끝난 것. } printf("\n\n"); } void mergeSort(int A[], int low, int high) { //(A, 0, n-1)을 받음. int mid; if (low < high) { //어차피 0, n-1이니까 갯수 확인 하는 용도. mid = (low + high) / 2; //계속 반을 나눔. mergeSort(A, low, mid); //재귀로 여기서 계속 들어감. 나눔...나눔..나눔.. //그러다보면 low와 mid가 같아질 떄가 있다. mergeSort(A, 0, 1)가 될 때 mergeSort(A, 0, 0)이 됌. //그러면 빠져나오기 시작함. mergeSort(A, mid + 1, high); //mergeSort(A, 0, 1)에서 한번 더 들어가면 //mergeSort(A, 0, 0) + mergeSort(A, 1, 1)가 나오고 둘다 안되므로 merge를 실행 merge(A, low, mid, high); //merge(A, 0, 0, 1)을 실행. //A[p]~A[k]와A[k+1]~A[q]를 합병함 } } 생각보다 헷갈렸다. 많이. 가령 5 1 9 2 7을 정렬한다고 치면 반으로 가르니까 5 1 9와 2 7로 나눠지고 3이면 1이니 1인덱스니까 5 1을 합병정렬해야된다. 1 5가 되고 그 다음에는 1 5 9. 그다음에는 2 7 그 다음에야 1 2 5 7 9가 된다. -시간복잡도 합병 정렬의 시간복잡도 = 층 수 * 각 층에서의 시간복잡도이다. 입력 크기가 n이라고 치면 n을 1/2로 계속 나누다가 더 이상 나눌 수 없는 1이 될 때 분할을 중단한다.n을 1/2로 k번 분할했을 때 1이므로 n=2^k로 k=〖log〗_2^n이다. 층이 생긴다. 각 층마다 n개의 원소가 복사되므로 O(n)이다. 〖log〗_2^n * O(n) = O(nlogn)이다. 분할의 수행시간 : 배열의 중간 인덱스 계산, 2번의 재귀 호출 O(1)시간 소요 합병의 수행 시간 : 입력의 크기에 비례함 • 2개의 정렬된 배열A와B의 크기가 각각 n과m이라면 , 최대비교횟수 = (n+m-1) 그러므로 O(n+m) 단점은 입력을 위한 메모리공간 외에 추가로 입력과 같은 크기의 공간이 별도로 필요하다. 연결 리스트에 있는 데이터를 정렬할 때 효율적임, 외부정렬의 기본이 됌. 문제를 2개의 부분문제로 분할, 각 부분문제의 크기가 일정하지 않은 형태의 분할 정복 알고리즘이다. 피봇(pivot)이라 일컫는 배열의 원소(숫자)를 기준으로 피봇보다 작은 숫자들은 왼편으로, 피봇보다 큰 숫자들은 오른편에 위치하도록 분할하고 피봇을 그 사이에 놓음. 퀵 정렬은 분할된 부분문제들에 대해 동일한 과정을 재귀적으로 수행하여 정렬함. QuickSort(A, left, right) 입력: 배열A[left]~A[right] 출력: 정렬된 배열A[left]~A[right] if ( left < right ) { //(A, 0, n)이므로 당연함. 그냥 2개 이상인지 확인하려고 하는 이유 피봇을 A[left]~A[right]중에서 선택하고, 피봇을 A[left]와 자리를 바꾼 후, //그냥 피봇이 제일 왼쪽에 있으면 된다. 단지 중간 원소를 바램. 피봇과 배열의 각 원소를 비교하여 //왼쪽에서는 피봇보다 큰 수와 오른쪽에서 피봇보다 작은 수를 동시에 차례대로 바꿔줌. 피봇보다 작은 숫자들은 A[left]~A[p-1]로 옮기고, 피봇보다 큰 숫자들은 A[p+1]~A[right]로 옮기며 피봇은 A[p]에 놓음 // 피봇보다 작으면서 가장 오른쪽에 있는 숫자와 피봇을 바꿔줌. j임. QuickSort(A, left, p-1) // 피봇보다 작은 그룹 QuickSort(A, p+1, right)// 피봇보다 큰 그룹 } <시간복잡도> 퀵 정렬의 성능은 피봇 선택이 좌우한다. 피봇으로 가장 작은 숫자 또는 가장 큰 숫자가 선택되면, 한 부분으로 치우치는 분할을 야기한다. 최선의 경우 시간복잡도 : O(n〖log〗_2^n) 각 층에서 비교 횟수 : O(n) ->각 원소가 각 부분의 피봇과 1회씩 비교됨. 총 비교 횟수 = O(n) * (층수) = O(n) * (n〖log〗_2^n) 평균의 시간복잡도는 최선의 경우와 같다. O(n〖log〗_2^n) 피봇을 항상 랜덤하게 선택한다고 가정했을 때. 최악의 경우에는 O(n^2)가 나온다. 퀵 정렬은 많은 입력에 대해 가장 좋은 성능을 보인다. 삽입정렬이 같이 사용되면 성능을 높일 수 있다. (입력) 데이터 간의 관계를 고려하지 않고 수행 과정에서 '욕심내어' 최소값 또는 최대값을 가진 데이터를 선택함. (근시안적 선택) 근시안적인 선택으로 부분적인 최적해를 찾고, 이들을 모아서 전체 문제의 최적해를 얻음. 선택한 데이터를 버리고 다른 걸 취하지 않음. 이러한 특성때문에 대부분의 그리디 알고리즘은 매우 단순하며, 제한적인 문제들만이 그리디 알고리즘으로 해결됨. *동전 거스름돈 문제 가장 간단하고 효율적인 방법 -> 남은 액수를 초과하지 않는 조건하에 욕심내어 가장 큰 액면의 동전을 취함. #include #pragma warning(disable: 4996) int main(void) { int change, W; int n500 = 0, n100 = 0, n50 = 0, n10 = 0, n1 = 0; printf("input the 거스름돈 : "); scanf("%d",&W); change = W; while (change >= 500) { change = change - 500; n500++; } while (change >= 100) { change = change - 100; n100++; } while (change >= 50) { change = change - 50; n50++; } while (change >= 10) { change = change - 10; n10++; } while (change >= 1) { change = change - 1; n1++; } printf("500 number is : %d coin\n", n500); printf("100 number is : %d coin\n", n100); printf("50 number is : %d coin\n", n50); printf("10 number is : %d coin\n", n10); printf("1 number is : %d coin\n", n1); printf("coin number : %d coin\n", n500 + n100 + n50 + n10 + n1); getchar(); getchar(); getchar(); getchar(); return 0; } 500원짜리 동전을 처리하는데 다른 동전을 거슬러 주는 것에 대해 전혀 고려하지 않는다. 그리고 항상 최소 동전 수를 계산하지는 못 함. 160원짜리 동전이 있고 거스름돈이 200원이면 160원 + 10원*4개라서 총 5개인데 100원만 쓰면 2개로도 가능함. *부분 배낭 문제 배낭 문제는 물건을 통째로 배낭에 넣어야 하지만, 부분 배낭 문제는 물건을 부분적으로 담는 것을 허용함. 부분 배낭 문제는 그리디 알고리즘을 이용하면 된다. '욕심을 내어' 단위 무게 당 가장 값나가는 물건을 배낭에 넣는 식. 다음으로 값나가는 물건을 '통째로' 배낭에 넣을 수 없게 되면, 배낭에 넣을 수 있을 만큼만 물건을 부분적으로 배낭에 담음 //가치를 정렬할 때 버블 정렬이 쓰였다. FractionalKnapsack 입력: n개의물건,각물건의무게와가치, 배낭의용량C 출력: 배낭에담은물건리스트L과배낭에담은물건의가치합v 1. 각물건에대해단위무게당가치를계산함 2. 물건들을단위무게당가치를기준으로내림차순으로정렬함(정렬된물건리스트:S) 3. L=∅, w=0, v=0 4. S에서단위무게당가치가가장큰물건x를가져옴 5. while ( (w+x의무게) ≤ C ) { // x를배낭에넣음 6. x를L에추가함 7. w = w + x의무게 8. v = v + x의가치 9. x를S에서제거함// 배낭에넣었으므로리스트에서제거함 10. S에서단위무게당가치가가장큰물건x를가져옴 } 11. if ((C-w) > 0) {// 배낭에물건을부분적으로담을여유가있음 //남아있는 은을 계산 12. 물건x를(C-w)만큼만L에추가함 13. v = v + (C-w)만큼의x의가치 } 14. return L, vw는 빈 가방. 초기값은 0. L 배낭에 담을 물건 리스트 W 배낭에 담긴 물건들 무게의 합 V 배낭에 담긴 물건들 가치의 합 시간복잡도 –Line 1:n개의물건각각의단위무게당가치계산O(n) –Line 2:물건의단위무게당가치를내림차순으로정렬O(nlogn) –Line 5~10:while-루프의수행은n번을넘지않음, 루프내부의수행은O(1) –Line 11~14:각각O(1) 알고리즘의시간복잡도:O(n) + O(nlogn) + (n x O(1)) + O(1) = O(nlogn) –문제의최적해속에부분문제의최적해가포함되어있고,부분문제의해속에그보다작은부분문제의해가포함되어있다. –이를최적부분구조(Optimal Substructure) 또는최적성원칙(Principle of Optimality)이라고한다. <동적 계획 (Dynamic Programming) 알고리즘> 그리디 알고리즘과 같이 최적화 문제를 해결하는 알고리즘 먼저 입력 크키가 작은 부분 문제들을 모두 해결한 후에 그 해들을 이용하여 보다 큰 크기의 부분 문제들을 해결하여, 최종적으로 원래 주어진 입력의 문제를 해결하는 알고리즘. 최적화 원리 한 문제의 해가 최적이면 그 문제를 이루는 부분 문제들의 해도 최적이다. 동적 계획 문제를 풀기 위해 고려할 사항들 문제가 최적화의 원리가 성립하는지 검사. 부분 문제를 정의한다. 우리가 구해야 하는 큰 문제는 어떻게 정의되는지 알아본다. 큰 문제와 작은 문제 간의 관계를 찾는다. 즉, 점화식을 구한다. 점화식을 통해 작은 문제들을 어떤 순서로 구해나갈 것인지를 결정한다. 답을 얻는 과정을 추적하는 방법을 세워야 한다. 분할 정복 알고리즘과 반대. 더 이상 분할할 수 없는 해(최소 단위)를 취합하여 그 위의 해를 구한다. 그리고 분할 정복 알고리즘은 부분 문제의 해를 중복 사용하지 않음. 그러나 동적 계획 알고리즘에는 부분 문제들 사이에 의존적 관계가 존재함. 대부분의 경우 뚜렷이 보이지 않아서 '함축적인 순서'라고 함. 배열을 이용하여 똑같은 문제의 해를 여러 번 구할 필요 없이 Table에 저장했다가 꺼내서 사용함(Memoization) 메모이제이션(Memoization) 동일한 계산을 반복해야할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복수행을 제거하여 프로그램 실행속도를 빠르게 하는 기술 동적 계획 알고리즘은 모든 문제에 대하여 항상 최적해를 찾음. *동적 계획 알고리즘을 이용한 동전 거스름돈 문제 동전 거스름돈 문제에 주어진 문제 요소들 –> 정해진 동전의 종류(n500, n100...), 거스름돈 n원 거스름돈 금액을 1원씩 증가시키며 문제를 해결함. -> 이전 단계를 이용해서 풀게 됨. 거스름돈 1원 C[1] = C[j-1] + 1 = C[1-1] + 1 = C[0] + 1 = 1 거스름돈 2원 = C[1] + 1 = 2 거스름돈 3원 = C[2] + 1 = 3 거스름돈 4원 = C[3] + 1 = 4 거스름돈 5원 = C[5] = 1 j원을 거슬러 받을 때 최소의 동전 수 -> C[j] 500원 짜리 동전이 필요하면 -> C[j-500] = C[j-d1] i가 1~k까지 변화하고 d1, d2, d3...dk 각각에 대하여 해당 동전을 거스름돈에 포함시킬 경우의 동전 수를 고려하여 최소값을 C[j]로 정함. C[j] = min{C[j-di] + 1}, if j >= di ///////////////////////////////////////// 입력 : 거스름돈 n원, k개의 동전의 액면, d1 >d2 > ...dk = 1 출력 : C[n] for i = 1 to n C[i] = 무한대 // 동전의 개수를 무한대로 해 놓은 것. C[0] = 0; for j = 1 to n{ for i = 1 to k{ //거스름돈 계속 확인해나가는 단계 if(di <= j) and (C[j-di] + 1 < C[j]) //di가 동전이니까. d1이 16원. 각각의 동전에 대해서 확인 하는 것. //동전의 액면가 보다 크다면. 1을 더했더니 바꿔줘야된다. 그게 뭐냐면 거스름돈 20원에다가 16원을 빼는 것. C C[j] = C[j – di] + 1; //최소의 동전 갯수로 갱신해 나가는 것. } } return C[n] //j : 1원부터 증가하는 임시 거스름돈 액수 j = n : 입력에 주어진 전체 거스름돈 액수 코드 #include #pragma warning(disable: 4996) #define MAX 100 int CoinChange(int arr[], int k, int n) { int C[MAX]; int i, j, x; for (i = 0; i <= n; i++) C[i] = 100; //원래 무한대인데 100개를 거슬러 준다고 생각. C[0] = 0; //거스름돈이 1일때 그에 맞는 동전을 찾아서 개수를 정의해주고 있다. for (j = 1; j <= n; j++) { //모든 거스름돈에 대해 확인 for (i = 1; i <= k; i++) { //모든 동전에 대해 확인 if ((arr[i] <= j) && (C[j - arr[i]] + 1 < C[j])) //거스름돈과 동전을 비교하고 있다. 동전이 큰 것부터 비교하다가 arr배열도 같은 게 나오면 C[0]이 되어버리면서 그 C[j]값은 1이 되어버린다. //16원과 거스름돈을 비교. //배열에 마이너스 값이 들어가든 인덱스 범위를 넘어선 값이든 그 값에 해당하는 메모리가 유효한 메모리라면 에러없이 거기에 아무값이나 넣어버린다. C[j] = C[j - arr[i]] + 1; } } return C[n]; } int main(void) { int arr[] = { 0, 16, 10, 5, 1 }; //0은 어차피 안 써서 넣어준 것.(어차피 1에서 시작) //큰 숫자의 순서로 해야 C[5], C[10] 등등이 먼저 정의되고 뒤 숫자(arr[1] = 16이면 10, 5, 1 얘네들)들이 들어갔을 때 조건에 걸려 정의된 얘들(C[16], C[10]등등)이 다시 정의되지 않는다. int k = sizeof(arr) / sizeof(arr[0]); int n = 20; //거스름돈 int result = CoinChange(arr, k - 1, n); printf("coin number = %d\n", result); /*for(int i = -20 ; i < 10 ; i++) printf("arr[%d] = %d\n",i, arr[i]);*/ getchar(); getchar(); getchar(); getchar(); getchar(); return 0; } *동적 계획 알고리즘을 이용한 배낭 문제 k[i, w]물건을 1~i까지 고려하고, (임시) 배낭의 용량이 w일 때의 최대 가치. K[n,C] : 최적해 입력의 크기가 너무 크면 고려를 많이 해야되고 복잡해 지므로 제한적이다. 배낭문제 첫 줄. 아래로 다 0을 만들어 줌. 이 물건을 넣을 수 있지만 6번을 해야됌. 그냥 위에 값을 그대로 가져옴. 물건을 담을 수 있을 때 ,K[i-1, w-wi] + vi는 K[0, 0] + 10이 된다. K[1-1, 5(배낭의 용량)-5(무게)] 배냥 용량이 10일 때 10밖에 안 들어가는 이유는 물건이 하나만 있기 때문에. max{?, ?} 에서 앞쪽 ?는 최소인데 바로 전 값을 넣어주는 것. 배낭용량 5에서 물건 2인 경우 40이 된 이유는 물건을 빼고 넣어줬기 때문에. 그게 더 효율적이라서. 가치가 10이니까 무게가. <7장 NP-완전 문제> 풀 수 있는 문제? VS 풀 수 없는 문제? // 쉬운 문제? VS 어려운 문제? 다항식 시간 알고리즘이 존재하는가? 다항식 시간 : 2n, 3n^2 비 다항식 시간 : 지수시간(2^n), 계승시간(n!) 실행할 때마다 정답이 없는 경우. NP문제 결정/판정 문제 -> 문제의 해가 'yes', 'no' 형태인 문제. 최적화 문제 주어진 문제에 대하여 하나 이상의 가능한 해들 중에서 가장 최적인 해답을 찾아야 하는 문제. P ? VS NP? 어려워 보이지만 답을 보면 옳다는 것을 검증하기 쉬운 문제.(수학 문제와 씨름하다가 포기하고는 답을 보고 이해하는 경우.) NP 알고리즘 비 결정적 다항식 시간 알고리즘 첫 번째 : 주어진 입력에 대해서 하나의 해를 '추측하고', 두 번째 : 그 해를 다항식 시간에 확인한 후, 그 해가 '맞다'고 답함. 결정 문제 NP 알고리즘은 해를 찾는 알고리즘이 아니라 해를 다항식 시간에 확인하는 알고리즘이다. NP – 완전(Complete) vs NP- 하드(hard) 문제 B는 쉽다. 문제 A는 Yes/no 대답이 일치하는 문제 B로 쉽게 변환된다. 문제 A도 쉬운가? NP완전 NP의 모든 문제가 NP의 어떤 문제 R로 다항식 시간 변환이 가능하다면 문제 R은 NP-완전(Complete) 문제임. 완전인 문제는 NP-완전 문제는 NP 하드 문제이면서 동시에 NP문제임. 완전인 문제는 NP의 가장 어려운 문제로 생각. NP하드 NP의 모든 문제가 문제 R로 NP-complete 문제 중에서 하나라도 p라는 것이 증명되면, NP-complete 문제는 전부 P가 되게 된다. 분류정도는 알아야 된다고 하심. 더 나은 알고리즘이 나올 수 있을거야. 아니면 더 이상 해보지 않는 것. 수도코드는 무조건 알고. 알고리즘 코드 빈칸. 1억이 1초. 시간복잡도. 원리나 증명보다는 그래프. Dfs bfs는 모든 정점을 한 번씩 방문하는 것. 두번 방문하면 dfs나 bfs가 아니다. 최단거리는 꼭 bfs로 Bfs 다음정점쪽에 왜 true를 줘야하는지? # my blog algo post # Algorithm basic ## Bubble Sort 이웃하는 숫자를 비교하여 작은 수를 앞쪽으로 이동시키는 과정을 반복 (마치 '거품’처럼 위로 올라감) ### Bubble sort pseudo code ```c //크기가 n인 배열 A for pass = 1 to n-1 // 총 n-1번을 비교해야 된다. (A[0]과 A[1], A[1]과 A[2] ...) for i= 1 to n-pass // 오름차순이면 이미 정렬 한 뒷 쪽 얘들은 정렬하지 않아도 되니까. if (A[i-1] > A[i])// 왼쪽의 원소가 오른쪽의 원소보다 크면 A[i-1] ↔ A[i]// 서로 자리를 바꿈 return 배열A ``` 총 비교 횟수는 (n-1) + (n-2) + … + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) ## Selection sort 배열 전체에서 최솟값을 '선택’하여 배열의 0번 원소와 자리를 바꾼다. 다음에 정렬할 때는 **0번 원소를 제외한** 나머지 원소에서 최솟값을 선택하여, 배열의 1번 원소와 자리를 바꾼다. 이런 식으로 반복한다. ### Selection sort pseudo code ```c //크기가 n인 배열 A for i= 0 to n-2{ // n-2까지 해야 밑의 for문에서 n-1까지 찾는다. min = i // min에 0값을 대입시켜서 다음 값과 계속 비교하게 함. for j = i+1 to n-1{ // while ( j >= h) and (A[j-h] > CurrentElement) { // j값이 바뀌지 않는 이상 왼쪽은 무조건 성립하고 오른쪽이 비교하는 것! 간격만큼의 값보다 큰지 작은지. 만약 왼쪽에 있는 원소가 더 크면 A[i]~A[n-1]에서 최솟값을 찾음 if (A[j] < A[min]) //min값이 그 다음 값보다 크면 min = j //min을 그 다음 값으로 바꿔줌. } A[i] ↔ A[min] // min의 원소 값과 처음에 했던 i값과 바꿈. } return 배열A ``` 총 비교 횟수는 : (n-1) + (n-2) + … + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) ## Insertion sort 배열을 정렬된 부분(앞 부분)과 정렬 안 된 부분 (뒷 부분)으로 나눈다. **(맨 처음에 첫 번째 원소는 정렬 되어있고 두 번째 원소부터 정렬 안 되어있다고 생각하면 쉽다.)** 정렬 안 된 부분의 가장 왼쪽 원소를 정렬된 부분의 가장 오른쪽 원소와 비교하는데 정렬 안 된 부분의 가장 왼쪽 원소가 정렬된 부분의 가장 오른쪽 원소보다 작으면 정렬 된 부분의 가장 오른쪽 원소가 오른쪽으로 한 칸 씩 옮겨진다. 그러다가 그러지 않는 곳이 있으면 그곳에 정렬 안 된 부분의 가장 왼쪽 원소를 넣는다. ### Insertion sort pseudo code ```c //크기가 n인 배열 A //얘도 bubble sort와 마찬가지로 정렬된 부분과 정렬 안 된 부분 기준이므로. n-1번 for i=1 to n-1 { CurrentElement= A[i] // 정렬 안 된 부분의 가장 왼쪽 원소를 element에 따로 넣음 j ← i-1// 정렬된 부분의 가장 오른쪽 원소 인덱스를 j에 넣음 while (j >= 0) and (A[j] > CurrentElement) { // j가 0보다 작으면 마이너스 인덱스라 안 되고 거기까지 비교해야 되니까, 정렬된 부분의 가장 오른쪽 원소가 정렬 안 된 부분의 가장 왼쪽 원소보다 크다면 A[j+1] = A[j] // 바로 오른쪽에 정렬된 부분의 가장 오른쪽 원소값을 대입시켜서 오른쪽으로 옮겨버림 j ← j-1 //그리고나서 j를 하나 깎고 다시 while문으로. } A [j+1] ← CurrentElement // A[j]가 더 작으니까 그 다음 자리가 비었을 테니 거기다가 element를 넣어줌. } return A ``` 총 비교 횟수는 : (n-1) + (n-2) + … + 2 + 1 = n(n-1)/2 이고 if문에서 바꾸는 시간복잡도는 O(1)이므로 전체 시간복잡도는 O(n^2) * O(1) = O(n^2) ## Shell sort bubble sort와 insertion sort의 단점을 보완함. 배열 뒷 부분의 작은 숫자를 앞 부분으로 이동시키고 동시에 앞 부분의 큰 숫자는 뒷 부분으로 이동시키고. 간격이 1일 때는 삽입 정렬을 수행한다. 간격을 설정하고 그 다음 간격은 더 작게 설정한다. 그리고 마지막에는 간격을 1로 놓고 수행한다. 쉘 정렬의 수행 속도는 간격 선정에 따라 좌우 된다. ### Shell sort pseudo code ```c for each gap h = [ h0> h1> ... > hk=1] //갭을 지정한다. for문을 써서 /2를 해 계속 낮춰줄 수 있다. 마지막은 1이라 삽입정렬이 되도록. for i = h to n-1{ //h부터 n-1까지. 밑에 j-h가 있어서 h이면 0이되기 때문에 간격만큼 비교하는 게 된다. CurrentElement = A[i] ; //차례대로 current에 넣는다. 바꿔주려고. j = i ; // 위와 동일한 인덱스를 주기 위해서 i를 j에 넣음. while ( j >= h) and (A[j-h] > CurrentElement) { // j값이 바뀌지 않는 이상 왼쪽은 무조건 성립하고 오른쪽이 비교하는 것! 간격만큼의 값보다 큰지 작은지. 만약 왼쪽에 있는 원소가 더 크면 A[j] = A[j-h]; //오른쪽 원소에 왼쪽 원소를 넣는다. j ←j-h; //오른쪽 원소 인덱스를 왼쪽 원소 인덱스로 바꿔준다. 얘가 간격만큼 빠지니까 그 다음도 간격만큼 빠지는 값을 봐야 된다. j >= h가 만족하고 A[j-h] > current면 또 바꿔줄 수 있다. } A[j] ←CurrentElement; //그러면 바로 currnet를 A[j]에 넣어줌으로써 완전히 바뀌게 된다. } return A ``` 최악의 경우 시간복잡도 : O(n^2) 최선의 경우 시간복잡도 : O(nlogn) 쉘 정렬의 시간복잡도는 아직 풀리지 않은 문제다. 쉘 정렬은 입력 크키가 크지 않은 경웨 매우 좋은 성능을 보인다. 보통 Embedded 시스템에서 주로 사용한다. ## Heap sort 힙 조건 **(각 노드의 값이 자식 노드의 값보다 커야/ 작아야 함)** 을 만족하는 완전 이진 트리(트리의 원소를 왼쪽에서 오른쪽으로 하나씩 빠짐없이 채워나간 형태). 노드들을 빈 공간없이 배열에 저장한다. 힙의 루트에는 우선순위가 가장 높은 값이 저장됨. (가장 큰 값 또는 가장 작은 값) ### 힙에서 부모 노드와 자식 노드의 관계 1. A[0]은 비우고 A[1] ~ A[n]까지 힙 노드들을 트리의 층별로 좌에서 우로 배열에 저장한다. 2. A[i]의 부모노드 = A[i/2] 3. A[i]의 왼쪽 자식 노드 = A[2i] 4. A[i]의 오른쪽 자식 노드 = A[2i + 1] ### 오름차순 정렬 시 힙의 루트에는 가장 큰 수가 저장된다. 그리고 루트의 숫자를 힙의 가장 마지막 노드에 있는 숫자와 바꾼다. 바꾼 마지막 노드의 숫자가 힙 조건에 위배되면 힙 조건을 만족시켜야 하니 밑의 자식들 중에서 큰 자식과 바꾸고(DownHeap()), 루트를 고정시키기 위해 힙 크기를 1개 줄이는 식으로 해서 반복한다. . ### Heap sort pseudo code ```c heapSize = n // heapSize: 힙크기 for i = 1to n-1 A[1] ↔ A[heapSize]// 루트와힙의마지막노드교환 heapSize = heapSize-1// 힙의크기를1 감소 DownHeap()// 위배된힙조건을만족시킴 return 배열A ``` ### Heap sort C code ```c #include #include #pragma warning(disable:4996) void DownHeap(int *arr, int i) { int j, n, temp, flag = 1; n = arr[0]; while ((i * 2 <= n) && (flag == 1)) { j = i * 2; //왼쪽 자식의 인덱스 if ((j + 1 <= n) && (arr[j] < arr[j + 1])) j = j + 1; //오른쪽 자식의 인덱스 if (arr[j] < arr[i]) flag = 0; else { temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i = j; } } } void BuildHeap(int *arr) { //빅오의 n으로 볼 수 있다. int i, n; n = arr[0]; //그대로 main함수에 있던 n. 즉, size가 들어간다. for (i = n / 2; i >= 1; i--) //n의 반 만큼 횟수를 지정. 이유는 노드가 그 만큼이기 때문에 9개면 4번 실행하면 되니까. DownHeap(arr, i); //4,3,2,1 순서로 실행해준다. 즉, 완전 이진트리인지 확인하는 거. } int main(void) { int n, A[20], i, j, last, temp; printf("array size : "); scanf("%d", &n); printf("\n input the array : "); for (i = 1; i <= n; i++) scanf("%d", &A[i]); A[0] = n; //아예 사이즈를 A[0]에다 넣고있음. BuildHeap(A); //BuildHeap함수 실행 printf("\n max heap \n"); for (i = 1; i <= n; i++) printf("%d", A[i]); while (A[0] > 1) { last = A[0]; //사이즈(끝값)를 last에 넣음 temp = A[1]; //끝 값과 Root를 바꿔주는 과정 A[1] = A[last]; A[last] = temp; A[0]--; //그리고 heap을 하나 줄여주기 위함. 9에서 8으로... DownHeap(A, 1); //8,1을 넣어서 실행시킴. 그리고 나오면 다시 반복. printf("\n\n middle value result \n"); for (i = 1; i <= n; i++) printf(" %d", A[i]); } printf("\n ** array result ** \n"); for (i = 1; i <= n; i++) printf("%d", A[i]); getchar(); return 0; } ``` ### Divide and Conquer Algorithm 주어진 문제의 입력을 분할하여 문제를 (정복)해결하는 방식. 분할한 입력에 대하여 동일한 알고리즘을 적용하여 해를 계산하며, 이들의 해를 취합하여 원래의 해를 얻음. 부분 문제는 더 이상 분할할 수 없을 때까지 분할함. 크기가 n인 입력을 3개로 분할한 경우(각 분할된 부분 문제의 크기) -> n/3(입력 크기가 1인 경우, 더 이상 분할하지 않음) 입력 크키가 n일 때 총 몇 번을 분할하면 더 이상 분할할 수 없는 크기인 1이 될까? -> 총 분할한 횟수를 k, 1/2씩 분할된다고 하면… 1번 분할했을 때 n/2, n/2^2, n/23…n/2k가 된다. n/2^k가 = 1일 때 더 이상 분할할 수 없다. ## Merge Sort Merge sort는 분할 정복 알고리즘으로 2개의 각각 정렬된 숫자들을 1개의 정렬된 숫자들로 합치는 것이다. 입력을 2개의 부분 문제로 분할, 부분 문제의 크기가 1/2로 동일하게 감소한다. (재귀적으로 나눌 수 없을 때까지 분할) -> 각각의 부분 문제를 **재귀적**으로 합병하여 정렬(정복)한다. ### Merge sort pseudo code ```c MergeSort(A,p,q) 입력: A[p]~A[q] // 출력: 정렬된A[p]~A[q] if ( p < q ) { // 배열의 원소의 수가 2개이상이면 //어차피 p에 0이 들어가고 q에 n-1이 들어가기 때문에 저 공식을 써도 됌. k = (p+q)/2 // k; 반으로 나누기 위한 중간 원소의 인덱스 MergeSort(A,p,k) // 앞 부분 재귀호출 MergeSort(A,k+1,q) // 뒷 부분 재귀호출 Merge(A, p, k, q) // A[p]~A[k]와A[k+1]~A[q]를 합병함 } ``` ### Merge sort C code ```c #include #pragma warning(disable: 4996) #define MAX 50 void merge(int*, int, int, int); void mergeSort(int*, int, int); int main(void) { int A[MAX], i, n; printf("array size : "); scanf("%d", &n); printf("\n input the array : "); for (i = 0; i < n; i++) scanf("%d", &A[i]); mergeSort(A, 0, n-1); //mergeSort함수로 감. printf("\n \n"); for (i = 0; i < n; i++) printf("%d ", A[i]); //A배열을 출력함. getchar(); return 0; } void merge(int A[], int low, int mid, int high) { int i, m, k, l; int temp[MAX]; //temp는 A와 크기가 같아야 함. 공간을 따로 둬야된다. 똑같은 게 있어야됌 l = low; //처음부터 확인 i = low; //temp의 처음부터 시작해야되니까 m = mid + 1; //mid의 바로 다음 것부터 시작. (중간 다음부터 비교를 해야되니까.) while ((l <= mid) && (m <= high)) { //맨 처음이 중간을 지났냐?, 중간다음이 맨 끝을 지났냐? if (A[l] <= A[m]) { //인덱스가 아니라 원소다 착각하지말자. 맨 처음과 중간 다음을 비교함. temp[i] = A[l]; //맞으면 천천히 대입하면서 증가시킨다. l++; //증가시키고 다시 돌린다. 대입했으니 그 다음으로. printf("LEFT : temp[%d]=%d\n", i, temp[i]); } else { temp[i] = A[m]; //만약 m쪽이 더 작으면 그걸 다음 원소에 넣어줘야지. m++; //증가시켜주고. printf("RIGHT : temp[%d]=%d\n", i, temp[i]); } i++; //다음 원소 바꾸거나 넣어주려고. } if (l > mid) { //왼쪽 부분은 다 했으니까 m이 커졌던 부분에서부터 오른쪽 값을 temp에 복사 for (k = m; k <= high; k++) { //mid+1을 넣고 high까지 temp[i] = A[k]; i++; printf("RIGHT : temp[%d]=%d\n", k, temp[i-1]); } } else { //오른쪽 부분이 완료됐으니 커진 l에서부터 왼쪽 값을 temp에 복사 for (k = l; k <= mid; k++) { // l을 증가시키면서 그 원소값을 넣어줌. temp[i] = A[k]; i++; printf("LEFT : temp[%d]=%d\n", high, temp[i-1]); } } printf("RESULT : "); for (k = low; k <= high; k++) { A[k] = temp[k]; //temp에다 넣어줬던 걸 다시 A[]에 넣는 과정. printf("%d ", A[k]); //이제 merge가 하나 끝난 것. } printf("\n\n"); } void mergeSort(int A[], int low, int high) { //(A, 0, n-1)을 받음. int mid; if (low < high) { //어차피 0, n-1이니까 갯수 확인 하는 용도. mid = (low + high) / 2; //계속 반을 나눔. mergeSort(A, low, mid); //재귀로 여기서 계속 들어감. 나누고 나누고 나누고... //그러다보면 low와 mid가 같아질 때가 있다. mergeSort(A, 0, 1)가 될 때 mergeSort(A, 0, 0)이 됌. //그러면 다시 하나하나씩 빠져나온다.빠져나오기 시작함. mergeSort(A, mid + 1, high); //mergeSort(A, 0, 1)에서 한번 더 들어가면 //mergeSort(A, 0, 0) + mergeSort(A, 1, 1)가 나오고 둘다 안되므로 merge를 실행 merge(A, low, mid, high); //merge(A, 0, 0, 1)을 실행. //A[p]~A[k]와A[k+1]~A[q]를 합병함 } } ``` 생각보다 헷갈렸다. 많이. 가령 5 1 9 2 7을 정렬한다고 치면 반으로 가르니까 5 1 9와 2 7로 나눠지고 3이면 1이니 1인덱스니까 5 1을 합병정렬해야된다. 1 5가 되고 그 다음에는 1 5 9. 그다음에는 2 7 그 다음에야 1 2 5 7 9가 된다. ### 시간복잡도 합병 정렬의 시간복잡도는 층 수 * (각 층에서의 시간복잡도)이다. 입력 크기가 n이라고 치면 n을 1/2로 계속 나누다가 더 이상 나눌 수 없는 1이 될 때 분할을 중단한다.n을 1/2로 k번 분할했을 때 1이므로 n=2^k로 k=logn이다. 각 층마다 n개의 원소가 복사되므로 O(n*logn)이다. - O(n) = O(nlogn)이다. 연결 리스트에 있는 데이터를 정렬할 때 효율적임, 외부정렬의 기본이 된다. ## Quick Sort 문제를 2개의 부분문제로 분할, 각 부분문제의 크기가 일정하지 않은 형태의 분할 정복 알고리즘이다. 피봇(pivot)이라 일컫는 배열의 원소(숫자)를 기준으로 피봇보다 작은 숫자들은 왼편으로, 피봇보다 큰 숫자들은 오른편에 위치하도록 분할하고 피봇을 그 사이에 놓는다. 퀵 정렬은 분할된 부분문제들에 대해 동일한 과정을 재귀적으로 수행하여 정렬한다. ### Quick sort c code ```c void quickSort(int arr[], int left, int right) { int i = left, j = right; //pivot을 중간으로 선택. int pivot = arr[(left + right) / 2]; int temp; do { while (arr[i] < pivot) //pivot보다 작으면 계속 증가 i++; while (arr[j] > pivot) //pivot보다 크면 계속 감소 j--; if (i <= j) { //빠져나온 것에서 서로 비교해서 바꾼다. temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i++; j--; } } while (i<= j); /* recursion */ if (left < j) //left 값보다 작아질 때까지 재귀함수를 돌린다. quickSort(arr, left, j); if (i < right) quickSort(arr, i, right); }} ``` ### 시간복잡도 퀵 정렬의 성능은 피봇 선택이 좌우한다. 피봇으로 가장 작은 숫자 또는 가장 큰 숫자가 선택되면, 한 부분으로 치우치는 분할을 야기한다. 최선의 경우 시간복잡도 : O(n) 각 층에서 비교 횟수 : O(n) ->각 원소가 각 부분의 피봇과 1회씩 비교됨. 총 비교 횟수 = O(n) * (층수) = O(n) * (n) 평균의 시간복잡도는 최선의 경우와 같다. O(n) 피봇을 항상 랜덤하게 선택한다고 가정했을 때. 최악의 경우에는 O(n^2)가 나온다. 퀵 정렬은 많은 입력에 대해 가장 좋은 성능을 보인다. 삽입정렬이 같이 사용되면 성능을 높일 수 있다. ## Greedy Algorithm 입력 데이터 간의 관계를 고려하지 않고 수행 과정에서 ‘욕심내어(greedy)’ 최소값 또는 최대값을 가진 데이터를 선택한다. 경우의 수를 모두 분석해서 부분적인 최적해를 찾고, 이들을 모아서 전체 문제의 최적해를 얻음. 선택한 데이터를 버리고 다른 걸 취하지 않음. 이러한 특성때문에 대부분의 그리디 알고리즘은 매우 단순하며, 제한적인 문제들만이 그리디 알고리즘으로 해결된다. ### 동전 거스름돈 문제 가장 간단하고 효율적인 방법 -> 남은 액수를 초과하지 않는 조건하에 욕심내어 가장 큰 액면의 동전을 취함. ```c #include #pragma warning(disable: 4996) int main(void) { int change, W; int n500 = 0, n100 = 0, n50 = 0, n10 = 0, n1 = 0; printf("input the 거스름돈 : "); scanf("%d",&W); change = W; while (change >= 500) { change = change - 500; n500++; } while (change >= 100) { change = change - 100; n100++; } while (change >= 50) { change = change - 50; n50++; } while (change >= 10) { change = change - 10; n10++; } while (change >= 1) { change = change - 1; n1++; } printf("500 number is : %d coin\n", n500); printf("100 number is : %d coin\n", n100); printf("50 number is : %d coin\n", n50); printf("10 number is : %d coin\n", n10); printf("1 number is : %d coin\n", n1); printf("coin number : %d coin\n", n500 + n100 + n50 + n10 + n1); getchar(); return 0; } ``` 500원짜리 동전을 처리할 때 다른 동전을 거슬러 주는 것에 대해 전혀 고려하지 않는다. 그래서 항상 최소 동전 수를 계산하지는 못 함. 만약 160원짜리 동전이 있고 거스름돈이 200원이면 100원보다 큰 160원 + 10원*4개라서 총 5개인데 중간에 있는 100원만 쓰면 2개로도 가능하다.