about. What I learned/about.Algorithm

지하철을 운행하기(from 선생님)

문제
장승배기, 연신내, 강남, 양재, 평택 이렇게 다섯개의 정거장이 있습니다. 지하철은 4개의 차량을 가지고 있고 이 지하철은 장승배기를 시작으로 평택까지 갔다가 다시 장승배기로 돌아옵니다. 단, 이 지하철은 1개의 역씩만 이동합니다.반복 노선이기에 종료하지 않는 이상 지하철은 계속해서 운행 됩니다. 총 4개의 차량은 각각 4명의 인원만 탑승할 수 있습니다. 또한 탑승시 목적지를 설정하며 설정한 목적지에 도착하게 되면 하차합니다. 

위의 문제를 분석해서 간단한 기능들을 찾아봅시다.

 

 

탑승 : 가정은 이러합니다. 한사람이 탈때마다 탑승하고자하는 차량을 선택하고 목적지를 선택합니다. 그렇게 한사람 탑승이 완료되면 다음 사람이 과정을 반복하며 탑승하면 좋겠습니다. 또한 차량의 인원이 모두차면 탑승이 불가능하게 기능이 구현됬으면 좋겠습니다. 

 

다음역 이동 : 다음역으로 갈때마다 해당 목적지를 하차지로 선택한 모든 승객들은 자동적으로 하차했으면 좋겠습니다.

## 가장 중요한 부분은 다음역으로 이동하다가 평택에 다다르면 다시 출발 방향이 바뀌어 장승배기에 도착해야합니다.

 

상세보기 : 이 기능을 실행하게되면 현재 어떤 차량에 누가 탑승하고있는지 확인할 수 있으면 좋겠습니다.


 

우선 메인이 포함된 클래스를하나 만들고 이 클래스에서는 동작만 할수 있게 짧게 코드를 작성합니다. 어찌보면 객체지향 언어의 의미를 잘 사용한 느낌인 것 같습니다. 다시 본론으로 돌아오면 사용자는 현재 클래스 안에서는 어떤일이 벌어져 실행이 되는지 전혀 알 수가 없습니다.

import com.gd.test.service.SubwayService;

public class SubwayController {

	public static void main(String[] args) {
		SubwayService ss = new SubwayService();
		
		System.out.println("---- Welcome Subway ----");
		
		ss.run();
	}

}

그저 서브웨이 서비스에 대한 위치를 임포트문으로 표시해줍니다. 그리고 서비스 객체를 생성한뒤 실행시켜주면 태고 내리고 자동으로 역이 옮겨집니다.

 

위의 메인클래스에서 정확한 값을 얻기 위해서는 아래와 같은 메서드들이 필요합니다. 굉장히 긴 클래스이지만 하나씩 잘라 분석해 보겠습니다.

public class SubwayService {
	String[] station = {"장승백이","연신내","강남","양재","평택"};
	int now = 0;
	int[][] trail = {{-1, -1, -1, -1},
					 {-1, -1, -1, -1},
					 {-1, -1, -1, -1},
					 {-1, -1, -1, -1}};
	int pos = -1;
	Scanner sc = new Scanner(System.in);
	

 

우선 지하철의 기능들을 구현해 놓을 main 메서드가 없는 클래스를 생성합니다.

 

이 지하철 문제를 풀기 위해서 문제를 분석하여 필요한 데이터들의 자료구조를 잡아냅니다. 

 

다섯개의 정거장을 가지고 있고 이 정거장은 늘어나거나 줄어들지 않습니다. 따라서 배열로 생성합니다. 역을 이동할 때 우리가 역의 위치를 알아낼 수 있는 방법은 인덱스 번호입니다. 인덱스 번호가 0번일때는 장승배기를 나타내고 4일때는 평택을 나타냅니다. 따라서 우리가 현재 위치한 역을 알기위해선 인덱스 값을 저장할 공간이 필요합니다. 그게 바로  int now =0  입니다.

 

역에 대한 자료들을 정리했고 이제는 열차 차량에 대한 데이터 자료가 필요합니다.

지하철 차량은 4량으로 고정되어 있고 한 량에 탈 수 있는 인원도 4명으로 제한되어 있습니다. 따라서 가변적이지 않기 때문에 배열로 공간을 고정해줍니다. -1 이라고 지정한 이유는 우리는 탑승하는 인원의 목적지를 배열에 저장하려 합니다.

 

목적지를 각 2차원 배열의 값으로 지정한다 그랬는데 잘 보시면 배열의 타입은  integer입니다. 이유는 이러합니다. 위에서 배열에 지정되어 있는 지하철 역을 찾아낼 수 있는 방법은 인덱스 번호라고 이야기했습니다. 따라서 각 각 차량의 인덱스 위치에 목적지의 인덱스번호를 입력하게되면 인덱스 번호를 뽑아내서 station[]에 입력해준다면 목적지를 찾아낼 수 있습니다.

 

자 이제 자료에 대한 정리는 끝났습니다. 

 

아 pos는 나중에 나올겁니다. 먼저 이야기를 살짝하자면 방향에 대한 변수 입니다. 

 

public void run() {
		boolean r = true;
		
		while (r) {
			System.out.println("=================================");
			System.out.println("현재역은 " + station[now] + "입니다.");
			System.out.println("=================================");
			System.out.println("메뉴를 선택하세요.");
			System.out.println("1.탑승 2.상세보기 3.이동 9.종료");
			
			switch(sc.nextLine()) {
			case "1" : join();
					   break;
			case "2" : status();
			break;
			case "3" : move();
			break;
			case "9" : r = false;
					   System.out.println("열차운행을 종료합니다.");
					   break;
			default : System.out.println("잘못입력하였습니다.");
			}
		}
	}

 

 

다음은 지하철을 운행할 메서드 입니다. 맨위의 메인 메서드가 존재하는 곳에서 실행되는 메서드입니다. 이 메서드 하나만 가지고 있어도 지하철을 움직일 수 있습니다.

 

while문을 무한 루프로 돌려서 일부러 끝내지 않는 이상 지하철은 계속해서 운행됩니다. 

 

 

public void join() { // 탑승 메서드
		int imp = 0;
		System.out.println("---- 탑승가능 현황 ----");
		
		for(int i = 0 ; i < trail.length ; i++) { 
			System.out.print((i + 1) + "호차 : "); 	
												   
			
			if(checkTrail(i) > 0) { 
				System.out.println("가능");
			} else {
				System.out.println("불가능");
				imp++;
			}
		}
		if(imp == trail.length) {
			System.out.println("탑승가능 열차가 없습니다.");
		} else {
			System.out.println("어느 열차에 탑승하시겠습니까?");
			
			for(int i = 0 ; i < trail.length ; i++) {
				System.out.print((i + 1) + "." + (i + 1) + "호차 ");
			}
			System.out.println();
			
			String input = sc.nextLine();
			int t = Integer.parseInt(input) - 1;
			if(checkTrail(t) > 0) {
				System.out.println("목적지를 선택해 주세요.");
				for(int i = 0 ; i < station.length ; i++) {
					if(now != i) {
						System.out.print((i + 1) + "." + station[i] + " ");
					}
				}
				
				System.out.println();
				String input2 = sc.nextLine();
				int s = Integer.parseInt(input2) - 1;
				
				if(s == now) {
					System.out.println("목적지로 현재역은 안됩니다.");
				} else {
					for(int i = 0 ; i < trail[t].length ; i++) {
						if(trail[t][i] == -1) {
							trail[t][i] = s;
							break;
						}
					}
				}
				
			} else {
				System.out.println("탑승이 불가능합니다.");
			}
			
		}
	}

첫번째 분석해볼 메서드는 탑승 메서드입니다. 

 

탑승 전에 확인해야할 부분이 있습니다. 지금은 아무 값도 들어가 있지 않지만 지하철을 운행하다보면 차량이 만석일 때가 있습니다. 그럴때를 대비해서 모든 차량을 확인하고 자리가 있는 차량을 알려주는 장치가 있다면 아주 좋을 것 같습니다. 이 장치가 있을 가장 좋을 때는 탑승하기 전일 것입니다.

 

위의 우리가 가지고있는 자료를 보면 2차원 배열을 가지고 있고 빈공간은 -1이라는 숫자로 표시해 놨습니다. 그렇다면 -1이라는 숫자의 개수가 빈자리의 개수가 되겠지요. 1번 차량에 모든 배열 인덱스를 포문을 통해 확인하고 -1이 있을때 카운트를 올린다면 해당 차량에 빈자리를 알 수 있습니다. 맨 처음 아무도 탑승하지 않았을 때 -1의 개수를 센다면 4일 것입니다. 이 말의 의미는 -1의 개수를 세었을 때 4와 동일하다면 그 열차는 완전히 비어있는 열차이며 만약 -1의 개수가 2개라면 해당 차량에는 2개의 자리가 비어있는 것입니다. 이러한 생각을 바탕으로코드를 구현해 보겠습니다.

 

public int checkTrail(int num) { //몇번째 열에서 시작할 것인지에 대한 변수의 값을 받습니다. 
		int cnt = 0; // 빈공간을 담을 변수를 선언해 줍니다. 
		
		for(int target : trail[num]) { // trail[num]
			if(target == -1) {
				cnt++;
			}
		}
		
		return cnt;
	}
}

각 차량의 빈자리가 얼마나 있는지 확인할 수 있는 메서드입니다. 

빈자리를 카운트하기위한 변수를 선언해주고 사용할 때마다 변수의 값은 0 이어야하기 때문에 0이라고 값을 지정해줍시다.

다음 포문은 향상된 포문입니다. 저의 티스토리에 자세히 설명되있으니 검색으로 찾아 보시면 좋겠습니다. 간다하게 말하자면 해당 배열의 인덱스들을 순서대로 꺼내오고 target이라는 변수에 집어넣어서 확인하는 겁니다. 때문에 for문이 도는 동안 target 변수는 계속해서 초기화 되고 포문 안의 내용을 돕니다. 이 코드로 인해 -1이라는 숫자가 cnt에 차곡 차곡 쌓이게 됩니다.

이제 원하면 모든 차량의 빈자리를 세거나 각 차량별로 빈자리를 셀 수 있습니다.

 

이제부터는 살짝 복잡합니다. 한대 묶여있기 때문에 짤라서 확인하기는 힘들지만 자세히 보면 짤라서 확인이 가능합니다. 우선 'int imp' 는 생각하지 않겠습니다. 진행하다가 필요하기 때문에 해당 장소에서 이야기를 꺼내는게 이해에는 더 좋을 것 같습니다.

 

그 아래에 있는 포문을 출력하게 되면 

 

1호차  : 가능

2호차 : 가능

3호차 : 가능

4호차 : 불가능 

 

이런 식으로 나오게 됩니다. 4개의 차량의 16자리가 모두 만석이되면 탑승이 불가능하다고 말해주면 좋을 것 같습니다. 헛걸음하지 않게 말이죠. 따라서 4개의 차량 중 만석인 차량이 존재하면 imp라는 변수를 선언하여 누적시켜주는게 좋겠습니다. imp 의 값이 차량의 길이와 같다면 탑승이 불가능하다고 알려주는 거죠. 보기 편하겠죠? 

그렇게  if문 과 else문으로 묶은 다음에 imp에 따라 실행되는 문장을 다르게 해줍니다. 조금더 작은 범위인 imp의 값이 4가 되면 모든 열차가 가득 찼다는 알림을 if문에 주는 것이 조금더 편할 것 같습니다.( 대부분의 조건문은 범위가 작은 것부터 써주는게 좋습니다.)

 

다음 엘스문에서는 탑승가능한 차량이 있으므로 차량 선택을 유도합니다. 타량을 선택하게 되면 차량에는 있는 -1이 목적지의 인덱스로 변하면 됩니다. 그렇다면 차량 선택 다음에 와야할 코드들은 목적지에 대한 코드일 것입니다. 예를들어 3번째 정거장인 '강남'의 번호는 3번일 것입니다. 고객이 '3'이라고 입력을하게되면 2차원 배열안에 있는 -1은 station 배열의 강남이 위치한 인덱스 번호인 '2'라는 숫자로 변경되어야합니다. 하지만 우리가 입력받은 값은 3이기에 강남의 인덱스 번호를 가르키려면 3에서 -1을한 2의 값을 지정 받을 수 있게 해야합니다.

 

이 코드에서 헷갈릴 만한 부분은 차량번호를 받았는데 차량번호가 쓰일일이 한번 밖에 없습니다. 그래서 조금 혼동이 올 수도있는데 차량번호를 받은 이유는 해당 차량에 빈자리가 있는지 확인하기 위함입니다. 입력 받고 바로 아래에 checkTrail에 보시면 참조변수에 저희가 입력받을 변수를 지정해 놓았습니다. 체크 트레일에서 해당 차량을 입력받고 탑승이 가능하면 목적지를 받는 로직을 구현한거죠. 그렇게 원하는 차량을 받고 목적지를 입력 받으면 현재 정거장과 입력받은 정거장이 같은지 확인하는 이프문을 통과합니다. 통과하고 나면 해당 차량에서 가장 첫번째로 존재하는 -1을 찾아서 해당 역의 인덱스 번호로 변경해줍니다. 이렇게 되면 탑승을 위한 로직이 끝이 납니다.

 

 

이 로직에서 중요한 코드는 5개의 정차역을 지나가는 이 지하철이 우리가 원할때 멈추게 만드는 것입니다. 이말을 다른 말로 하면 우리가 멈추지않으면 계속 실행 될 수 있는 코드를 구현해야한다는 말입니다.

 

public void move() {
		
		if(now == station.length - 1 || now == 0) {
			
			pos *= -1;
			
		}
		
		now += pos;
		
		int cnt = 0;
		for(int i = 0 ; i < trail.length ; i++) {
			for(int j = 0 ; j < trail[i].length ; j++) {
				if(trail[i][j] == now) {
					cnt++;
					trail[i][j] = -1;
				}
			}
		}
		
		if(cnt > 0) {
			System.out.println(cnt + "명이 하차하였습니다.");
		}
	}

 

인덱스 번호로 보았을때 첫번째 역인 장승배기는 0번, 평택은 4번입니다. 그렇다면 똑같이 증가하다가 0번일때와 4번일때(or비교연산자)방향이 바뀌게 만들면 될 것 같습니다. 말은 쉽지만 참 논리적으로 수학적으로 구현하려면 어려운 부분이 참 많습니다. 

 

하지만 음수라는 개념을 잘 생각해보면 딱히 어렵지 않다고들 이야기를 합니다.(평생 알아왔던게 마이너스면 그냥 빼지는 거야 뿐인 저는 동의하지 못합니다.)  장승배기부터 시작하여 정방향으로 가게 되면 +1이 계속 더해집니다. 그렇다면 4일때 반대로 -1이 계속 더해지게 하면 되겠죠? 그렇게 되면 방향을 1로 놓고 계속 진행하다가 현재역의 인덱스가 4가 되면 방향을 -1로 바꾸고 계속해서 더해주면 좋을 것 같습니다. 

4일때는 -1을 더하면 3 그 다음 역으로 움직일때는 3이니깐 -1을 하면 2 이렇게 말입니다.

 

이러한 사고를 가지고 코드를 구현하면 위와 같이 됩니다. 저도 처음 시도해 보았을 때는 엄청 지저분하게 나왔고 위의 코드는 선생님 코드입니다. 

 

그리고 아래부분에는 깔끔하게 빠져나가는 인원이 몇명인지 체크해주는 코드를 통해 사용자가 자세히 알 수 있게 돕습니다.

 

이렇게 만든후 코드를 실행 시켜보면 아주 잘 돌아갑니다.

 

처음은 언제나 어렵다고 생각합니다. 자료구조를 짜고 알고리즘을 통해 코드를 빠르고 쉽게 구현할 수 잇는 방법을 찾아내고 그것들을 조합하고 눈으로만 봐도 쉽지 않습니다. 하지만 어려울 것을 알고 들어왔기 때문에 고통스럽거나 하지는 않습니다. 

 

역경이 다가오더라도 언제나 최선을 다해서 배우겠습니다. 

 

Win or learn never lose.
- Nelson Mandela-