class Solution {
fun productExceptSelf(nums: IntArray): IntArray {
var indexOfZero = - // 원소 0의 인덱스
var product = 1 // 0을 제외한 모든 곱
val answer = IntArray(nums.size)
for (i in nums.indices) {
// 0이 2개 이상 -> 모든 값이 0이 되기 때문에 stop
if (nums[i] == 0 && indexOfZero != -1) {
product = 0
break
} else if (nums[i] == 0 && indexOfZero == -1) { // 0을 처음 발견했을 때
indexOfZero = i
continue
}
product *= nums[i]
}
// 0이 하나라면, answer[indexOfZero] = 나머지 원소의 곱, 나머지는 0
if (product != 0 && indexOfZero != -1) {
answer[indexOfZero] = product
}
// 0이 없을 때
else if (indexOfZero == -1) {
for (i in answer.indices) {
answer[i] = product / nums[i]
}
}
return answer
}
}
코드2 (누적 알고리즘)
/**
* answer[i] = 왼쪽누적곱[i-1] * 오른쪽누적곱[i+1]
*/
class Solution {
fun productExceptSelf(nums: IntArray): IntArray {
val answer = IntArray(nums.size)
val leftPreProducts = IntArray(nums.size)
val rightPreProducts = IntArray(nums.size)
leftPreProducts[0] = nums[0]
rightPreProducts[nums.lastIndex] = nums.last()
// 오른쪽 방향으로 누적곱
for (i in 1 until nums.size) {
leftPreProducts[i] = leftPreProducts[i-1] * nums[i]
}
// 끝에서 왼쪽 방향으로 누적곱
for (i in nums.lastIndex - 1 downTo 0) {
rightPreProducts[i] = rightPreProducts[i + 1] * nums[i]
}
for (i in 1 until answer.lastIndex) {
answer[i] = leftPreProducts[i - 1] * rightPreProducts[i + 1]
}
answer[0] = rightPreProducts[1]
answer[answer.lastIndex] = leftPreProducts[answer.lastIndex - 1]
return answer
}
}
난이도가 medium 이었지만, 나 한정 hard였다. 신기하게도 어제는 아무리 문제를 뜯어보고 풀이를 맛봐도 알송달송했는데, 오늘은 이해가 된다.(쓰면서 알았다. 제대로 이해하진 못했다) 무의식에서 내 머리가 열심히 고생해준 것 같다ㅎ
처음 봤을 때 DP 같다는 생각이 들었다. 길이가 k인 배열에서 nums[k]를 지우냐, 지우지 않느냐에 따라서 가장 긴 wiggle sequence를 결정할 수 있을 것 같았다. 그래서 경우의 수를 따져보려고 했는데, 뭔가 복잡했다. 노트가 없었어서(^^;) 머리에서만 해결해려다 보니 그랬던 것 같다.
잘 모르겠어서 discussion을 찾아봤다. 리트 코드는 세계적으로 많은 유저들이 있어서 그런지, discussion이 활성화 돼있어서 참 좋다. 친절한 분들의 친절한 설명이 많다. 아무튼, 그런데 설명을 봐도 뭐가 뭔지 잘 모르겠다 싶었다. DP는 일반적으로 생각하는 방식과 달라서, 반대로 Greedy는 너무 직관적인 때가 많아서 어렵다는 생각이 든다. (대충 다 어렵다는 뜻)
풀이
다시 문제로 돌아와서, DP든 Greedy든 어떤 원소들은 지우는 게 항상 이득인 때가 있다.이전 원소와 값이 같거나, positive/negative의 상태가 같을 때가 그렇다. 지우지 않으면 풀이를 종료해야 하기 때문이다. 그런데, 그런 원소들을 다 지우면 자연스럽게 Wiggle Sequence만 남는다. 값이 같거나 상태가 같은 원소를 다 지워버렸기 때문이다.
예를 들어 배열을 이전 항과의 상대적 상태로 표현했을 때 (상태: +,-,0), 000+++-+--++--- -> 0+-+-+-가 된다. 즉, 첫 원소를 제외하고 0(이전 항과 같음)은 모두 제거되고, 연속된 '+'나'-'는 하나만 남게 된다. 따라서, 이 작업만 해주면 자연스럽게 정답을 찾게 된다
그런데 조금 생각해보니까 뭔가 이상하다. 위의 예시 000+++-+--++---에서 숫자로 직접 해보면, 연속된 상태에서도 어떤 값을 지우느냐에 따라서 길이가 달라진다. 나는 이 부분을 이해하는 데 애를 먹었고, 사실 완벽하게 이해하지 못했다. 그래서 아이디어로 설명을 대신해보려고 한다. 배열 a,b,....,c,d (a<b, c>d)가 있을 때, (a,b)와 (c,d)는 b<c, b==c, b>c 상관 없이 항상 길이 3으로 연결할 수 있다.
코드
class Solution {
fun wiggleMaxLength(nums: IntArray): Int {
if (nums.size <= 1) {
return nums.size
}
var answer = 1
var prevDiff = 0
for (i in 1 until nums.size) {
val diff = nums[i-1] - nums[i]
// 이전 두 수의 상태와 다른 경우
// 등호를 통해 첫 원소와 연속해서, [5 5 5 3]처럼 처음에 같은 수들이 나오는 것도 처리할 수 있다
if ((prevDiff >= 0 && diff < 0) || (prevDiff <= 0 && diff > 0)) {
answer++
prevDiff = diff
}
}
return answer
}
}
2. endTime이 가장 늦은(큰) 일부터 차례대로 최대한 늦게 일을 시작하도록 일의 시간을 확정 짓는다.
=> 따라서, 위 과정에 앞서 endTime 기준 내림차순으로 정렬한다.
3. 가장 빨리 시작하게 될 일의 startTime을 출력한다.
1. 일을시작하는 시간(startTime)과마지막 시간(endTime)으로 변환한다.
일을 시작하는 시간(startTime)과 마지막 시간(endTime)으로 변환한다.
ex) 걸리는 시간: 5, 끝내야 하는 시간 9 => endTime: 9 - 1 = 8, startTime: endTime - 5 + 1 = 4
이렇게 놓으면 endTime 바로 다음인 끝내야 하는 시간에 일이 끝나있게 된다.
data class Task(var start: Int, var end: Int)
repeat(taskSize) {
val (duration, dueTime) = readLine().split(" ").map(String::toInt)
taskList.add(Task(dueTime - duration, dueTime - 1))
}
2.endTime 값이 가장 큰일부터 차례대로 최대한 늦게 일을 시작하도록 배치한다.
가장 늦게 일어나기 위해서 모든 일을 데드라인에 최대한 가깝게 배치하며, endTime 값이 가장 큰 task부터 스케줄을 확정한다. task(currentTask)를 스케줄할 때 직전에 배치한 task(nextTask)와 비교해서 타임 라인이 겹친다면, currentTask를 nextTask 바로 왼쪽으로 배치한다. 이렇게 하면 현재 상황에서 currentTask를 최대한 늦은 시간에 스케줄한 것이 된다.
ex) currentTask: task C, nextTask: task D
단, currentTask를 왼쪽으로 옮기는 과정에서 startTime < 0이 된다면, 제시간에 일을 끝낼 수 없다는 의미가 된다.
var prevTask = taskList[0]
for (i in 1 until taskSize) {
val currentTask = taskList[i]
// prevTask과 currentTask의 시간이 겹치는 부분이 있거나, 통째로 currentTask가 더 오른쪽에 있는 경우
// -> currentTask를 겹치지 않게 왼쪽 옮긴다
if (currentTask.end >= prevTask.start){ // currentTask가 prevTask에 걸쳐있는 경우
val moveToLeftTime = currentTask.end - prevTask.start + 1
currentTask.end -= moveToLeftTime
currentTask.start -= moveToLeftTime
}
if (currentTask.start < 0) {
print(-1)
return
}
prevTask = currentTask
}
3. 가장 빨리 시작하게 될 일의 startTime을 출력한다.
위에서 수평선 기준 오른쪽에 있는 task부터 겹치지 않으면서 데드라인에 가깝게 배치하고 나면, 맨 왼쪽에 있는 task의 startTime이 일을 가장 늦게 시작할 수 있는 시간이 된다.
코드
import java.io.BufferedReader
import java.io.InputStreamReader
// start: 일 시작 시간, end: 마지막 시간 -> end+1 시간에는 작업이 끝나있다
data class Task(var start: Int, var end: Int)
fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
val taskSize = readLine().toInt()
val taskList = mutableListOf<Task>()
repeat(taskSize) {
val (duration, dueTime) = readLine().split(" ").map(String::toInt)
val task = Task(dueTime - duration, dueTime - 1)
taskList.add(task)
}
// 가장 늦게 끝낼 일이 먼저 오도록 정렬
taskList.sortByDescending { it.end }
// 비교 기준, 시간상 currentTask 바로 다음에 진행할 task
var nextTask = taskList[0]
for (i in 1 until taskSize) {
val currentTask = taskList[i]
// nextTask currentTask의 시간이 겹치는 부분이 있거나, 통째로 current가 더 오른쪽에 있으면
// 겹치지 않게 current가 nextTask 직전에 끝나게 왼쪽으로 옮긴다
if (currentTask.end >= nextTask.start){
val moveToLeft = currentTask.end - nextTask.start + 1
currentTask.end -= moveToLeft
currentTask.start -= moveToLeft
}
// currentTask를 0초 이전에 시작해야 하면 -1 반환
if (currentTask.start < 0) {
print(-1)
return
}
nextTask = currentTask // 비교 기준 갱신
}
print(taskList.last().start) // 가장 먼저 시작할 task의 시작 시간
}