rlaalswo01   6년 전

코드 첨부했습니다. 코드를 조금 장황하게 짜는 스타일인 것 같아 보시는데 불편하시다면 죄송합니다 ㅠㅠ

질문의 요지는 왜 벡터의 erase가 list의 erase보다 빠른가? 입니다.

세 가지 방법으로 풀어 제출해봤는데요.


방법 1.입력으로 들어오는 진영을 모두 벡터에 담는다. -> 벡터를 순차적으로 탐색하여, visited 배열에 표시가 안된 원소라면 visited 배열에 표시하고 해당 원소를 리스트에 담는다. 이때 cnt 1 증가. -> 리스트는 empty가 될 때까지 매번 벡터의 처음부터 끝까지 탐색하여 front와 연락이 가능한 원소들을 push_back 하며 visited 배열에 표시를 하고 pop_front.  즉 최악의 경우 테스트 케이스 당 입력벡터의 최대사이즈 3000의 제곱 탐색을 할 것으로 예상.

이 방법으로 문제를 풀었을 때, 약 1600 MS 의 소요시간으로 문제를 풀었습니다.

이 다음의 저의 생각은, 이미 연락 그룹에 속해버린 원소들은 입력받은 벡터에서 제거하면 탐색량을 줄일 수 있어 소요시간 또한 줄일 수 있겠다는 생각을 하였습니다. 하지만 벡터의 erase 보다는 리스트의 erase가 효율적일 것으로 생각하여 첨부한 코드처럼 문제를 해결해 보았습니다.

방법 2.  방법 1과 같은 방식이지만 입력을 벡터에 담지 않고 리스트에 담는다. -> 입력 리스트가 비어있지 않다면 입력 리스트의 front를 임시리스트에 담고 입력 리스트에선 제거. 이때 cnt 1 증가. -> 임시 리스트의 front와 연락이 되는 원소를 입력리스트의 처음부터 끝까지 탐색하여 push_back 하며 동시에 입력 리스트에서는 erase.  입력 리스트가 비어있지 않다면 반복.

하지만 이 코드는 2000 MS 가 넘는 더 많은 시간을 소요하여 문제를 해결했습니다.

방법 3.그리고 첨부한 코드와 같은 방식이지만 다시 벡터에 입력진영들을 담고 벡터가 빌때까지 erase하며 원소들을 제거해나가니 약 600MS 로 문제가 해결되었습니다. 일반적으로 자료구조를 배울때 리스트의 장점은 중간 삽입, 제거가 빠르다는 것인데 왜 이런 결과가 나오는지 궁금합니다.

lohaswinner   6년 전

우선 source style 의 호불호를 떠나서 it++ 부분은 전반적으로 권장되지 않는 것으로 알고 있습니다.

vector 의 iterator 는 pointer 에 가깝기 때문에 그래도 괜찮지만 list 의 iterator 는 그런 최적화가 어렵죠.

독립적인 문장에서 쓰시는 거라면 ++it 을 쓰시고, tempit 과 관련된 부분은 tempit = it++ 로 두 문장을 한 문장으로 묶으시는 것도 괜찮을 것 같습니다.


먼저 확인차 여쭤보자면 vector 를 가지고 pop_front 를 하셨다는 말씀은 v.erase(v.begin()) 을 하셧다는 말씀인가요?

그게 맞다면 통상적으로 vector 의 특성상 많은 시간을 요구하는 작업인 것은 맞습니다.


원소의 할당 및 해제 속도는 원소 형식, 여기서는 ele 의 크기 및 특성도 영향을 많이 받을 것 같습니다.

또한 할당을 할 때 push_back 을 쓰는 거라면 node 기반 container 인 list 는 vector 에 비해서 느릴 수 밖에 없고요.

동적할당과 해제는 내부적으로 복잡한 과정을 거치기 때문에 응용에서 드러나는 시간복잡도와는 다른 결과를 보여줄 수 있다는 점도 염두하시기 바랍니다.

chogahui05   6년 전

이 문제를 풀어보니. 대략적인 알고리듬이 이렇게 되더군요.

(1) 각각의 쌍에 대해서 인접하는지 판별한다.

(2) 그 후 처리.

즉, (1)은 반드시 한다는 소리입니다. 여기서 O(n^2)네요.

그 후 처리에 대해서 물어보시는 건데요.


사실 앞에 n^2가 붙은 이상, 그 후 처리가 n^2이던, n이던, 1이던 n^2에 지배되는 것도 있을 거고요.

게다가 List잖아요. 같은 n^2라도 앞에 붙는 상수가 같진 않겠죠..

(사실 코드를 보여주지 않아서 어떻게 퍼포먼스를 비교해 달라는지는 잘 모르겠습니다만..)

벡터 같은 경우는..

push_back을 호출하는 경우에, 일정 이상 차면 새롭게 2*size 만큼의 공간을 할당해요. 그런데 List는 하나씩 할당해 버리죠..

게다가 연속적이지도 않고요.

제가 봤을 때는 아마도 삽입 과정에서 상수 차이가 상당히 많이 나서

그 다음 연산이 (List에서 삭제를 한다/vector에서 삭제를 한다) O(1)이던.. O(n)이냐가 중요하지 않았던 거 같네요.


예를 들어서, 1번 연산에서 n^2만큼 걸리고 2번 Query당 n만큼 걸리는 연산을 n회 수행하면 2n^2지만

1번 연산에서 5n^2만큼 걸렸지만, 2번 Query당 5만큼 걸리는 연산을 n번 수행하면

5n^2 + 5n이죠. 이건 2n^2보다는 크게 증가하죠.. 만약에 Q당 n^2만큼 걸리냐.

chogahui05   6년 전

그러면 야기가 달라질지도 모르겠지만..

1번 연산을 수행했을 때 세타(n^2)고 

2번 연산까지 수행했는데도 세타(n^2)다.

그런데 1번을 수행 후, 3번 연산까지 수행했는데도 세타(n^2)다.

그런데 2번은 Q당 세타(n), 3번은 Q당 세타(1)이다. 지금 이런 질문을 하시는 거 같은데요..

그러면 사실상 수행 시간은 합친 수행시간이라고 해야 할까요?

n^2 앞에 붙는 상수에 의해서 결정되겠지요.. 심지어 프로그램은 3번만 하거나 2번만 하고 끝나지 않잖아요.

1+2를 하거나 1+3을 하기 때문에, 1을 안 하고 끝날 수도 없고요

더 쉽게 말하면요

f(x) = x^2 + (x)*x고

g(x) = 5x^2 + (5)*x입니다. 어떤 게 더 클까요? 의 질문인 거 같아요.

rlaalswo01   6년 전

두분 다 답변 감사드립니다. 일단 제 수준에선 두분의 답변에 대해서도 완벽히 이해하기 위해서는 좀 시간을 소요할 것 같습니다.

일단은 두분께서 다시 질문주신 부분에 대해 답변 드립니다.

lohaswinner

먼저 확인차 여쭤보자면 vector 를 가지고 pop_front 를 하셨다는 말씀은 v.erase(v.begin()) 을 하셧다는 말씀인가요?

-> 1, 3번은 입력을 담아놓은 벡터 한개와 리스트 한개를 사용합니다. 벡터에서 아직 그룹화되지 않은 원소가 있다면 그 원소를 빈 리스트에 담고, 그 리스트는 BFS 리스트로 동작합니다. 리스트의 front()와 같은 그룹이 될수 있는 벡터 원소들을 리스트에 push_back으로 리스트 뒤에 넣고, 리스트의 front()는 pop_front() 하여 제거됩니다. 벡터에선 원소 삭제가 이뤄지지 않습니다. 리스트의 front()와 그룹이될 수 있는 벡터 원소를 탐색하는 과정에서 1번은 벡터를 매번 처음부터 끝까지 탐색합니다. 2번은 입력을 벡터 대신 리스트에 담아 두개의 리스트를 사용합니다. bfs에 사용되는 리스트의 원소들과 그룹이 되는 원소들을 입력 리스트에서 erase 함으로써, 다음번에 다시 입력이 담긴 컨테이너를 탐색할때 이미 처리된 원소들은 제거되어 탐색 대상이 되는 컨테이너 사이즈를 지속적으로 줄여주려는 의도였습니다. 3번은 2번과 같은 방법을 1번처럼 한개의 리스트와 한개의 벡터를 사용하였다고 말씀드릴 수도 있고, 1번과 같은 방법인데 erase만 추가하였다고 볼 수도 있습니다.  

chogahui05  


코드를 제가 가독성이 떨어지고 길게 작성하는데다가, 말로서 충분히 설명이 된다고 생각하여 하나만 첨부했었습니다. 죄송합니다. 

1번은 1400 MS 가 소요되었고, 입력을 담은 컨테이너를 줄여 나가자는 아이디어에서 벡터보다 리스트가 더 적합할것이라고 판단하여 리스트를 사용했던 2번은 2300 MS, 같은 방법을 벡터로 구현한 3번은 690 MS 소요되었습니다.

chogahui05   6년 전

직접 소스를 봐 보니

생각보다 상당히 정교하게 짜셔서 놀랐네요.. 그렇게 해서 상수를 줄이신 거 같은데.. 상상도 못했습니다.


사실 vector의 erase와 List의 erase를 보면. 이미 지워야 할 위치를 알고 있는 경우에는

List 같은 경우 O(1)이 소요되는 반면에, vector는 최악의 경우에 O(n)이 걸려버립니다. 이 부분은 저도 부정하지 못하겠네요.

실제로 3번처럼 짜신 경우에 49번째 줄 맞나 싶은데요. v.erase(tempit)이 최대 몇 번 호출이 될까요? 사실 n번밖에 호출이 되지 않아요.

그 이상 호출되진 않죠.


그 이유는 지역 a가 Team 여러 개 중 하나에 속해있는데..

만약에 n+1번 이상 호출되면 어느 하나가 Team 2개 이상에 속해있다는 소리거든요. 그런데. 문제 특성도 그렇고

bfs 특성도 그렇고.. 한 번 방문한 정점은 다시 방문해버리지 않잖아요. 아마 rla님 의도 역시 그러했으리라 생각합니다.

chogahui05   6년 전

즉, v.erase(tempit)이 있어서 3중 for loop 같고 O(n^3)일 거 같은데.. 사실 그게 아니고요.

그러면 다른 문제를 봐야겠죠..


사실 rla님 코드에서는 삭제하는 것도 중요하고, 시간 복잡도에 차지하는 비율도 꽤 크겠지만.

그에 못지 않게 중요한 것은 몹의 정보를 저장한 어떤 것을 탐색하는 일입니다. list나 vector나 다음 원소를 탐색하는 일은 O(1)이 맞습니다.

list는 next 포인터를 따라 가면 될 거고요. vector는 그냥 다음 것으로 가면 되지요. 그런데

list가 연속적으로 저장이 되어 있다고 말하지는 않습니다. 하지만 벡터는 연속적으로 저장이 되어 있다고 말하지요.

연속적으로 저장이 되어 있지 않다 = 흩어져 있다.

인접한 공간을 탐색했을 때 가까이에 있으면 찾기 쉬울 겁니다. 그런데, 안 그러면 어렵겠지요.. 뭔가 얻어와야 하고..

이런 이유 때문에 그러지 않을까 생각되고요. 삽/삭이 자주 일어나는 환경에서는 List를 쓰는 게 맞긴 합니다만..

그에 못지 않게 random 탐색이 이루어 지는 경우에는 고민을 많이 해 봐야 합니다.

chogahui05   6년 전

제 답변이 이해가 안 가신다면..

https://stackoverflow.com/ques...

rlaalswo01   6년 전

칭찬해주셔서 몸둘바를 모르겠습니다 ^^

erase 하기 이전에 어차피 순차적으로 끝에서부터 탐색이 이뤄져서 리스트가 유리할거라 생각했는데

erase 가 반드시 필요한 경우 아니면 되도록이면 큐나 써야할 것 같습니다 ㅎ..

예전에 백준의 어떤 문제를 보면 A,B,C 변수를 인덱스로 삼아 메모이제이션 하는 문제가 있었는데

그 문제가 A,B,C의 총합이 늘 같은 수라 A,B 가 결정되면 C도 자연스레 결정되는 문제였어서, 다른 분들 코드를 보니

A,B 만 인덱스로 삼아서 풀었더라구요. 그런데 사실 계산량으로는 두 코드가 똑같은데

(전자도 굳이 C를 계산해서 구해내는 게 아니라 C는 자연스레 도출되기 때문에..)

메모리를 전자가 훨씬 많이 사용하다보니 (메모이제이션 배열이 3차원으로 한 차원 증가) 

그것만으로도 수행속도가 많이 느려지더군요. 메모리에 있는 데이터에 접근하는 속도에서 차이가 나는지,

아니면 배열을 메모리에 할당하는데 오래 걸렸던건진 또 모르겠네요...  


여튼 이 문제 이후로 종종

컨테이너만 바꿔서 제출을 해보곤 했는데, 생각보다 벡터가 erase 등에서도 나쁘지 않은 성능을

보여준다는 걸 느꼈고, insert나 erase 등의 이유로 덱이나 리스트등이 더 빠를 것 같다고 판단되는 경우도

실제로 돌려봤을땐 기대처럼 벡터보다 더 나은 성능을 보여주는 경우 못지않게, 그렇지 않은

경우도 많은 것 같다는 느낌을 받았습니다. 

링크 해주신 자료 잘 보겠습니다. 감사합니다!

chogahui05   6년 전

중간 삽입/삭제 >> 랜덤 탐색인 경우에는 야기가 달라지겠지만 

(기본적으로 O(n)이기 때문에 조세퍼스 2에서 erase 함수로 퉁치기에는 많이 어렵습니다.) 

이 문제에선 안 그러니까 그런 듯 싶구요..

음.. 3차원 배열.. 사실 덩어리가 큰 경우 비유를 이렇게 하시면 좋습니다. 공부해야 할 양이 많다. 혹은 시험 범위가 엄청나게 많다.

보통은, Chap1부터 Chap 10까지 정리하신다 쳤을 때

앞에는 Chap1이, 맨 뒤에는 Chap 10에 대한 내용을 정리하시잖아요.

시험 범위가 Chap 1부터 5까지다. 그러면 중간까지만 보면 되겠죠. 여기서 문제입니다. 차례대로 정리한 경우에..

우리는 Chap1부터 차례대로 보는 게 효율적이지..

Chap1을 공부했다가 5를 공부했다가. 다시 모르니까 다시 1을 보고.. 이러면 찾는 데 시간이 걸리겠지요.

저는 분명히 Chap1을 공부하고 있었는데.. 그 다음에는 2가 나올 건데..

왜 5가 나오는 위치를 찾냐.. 이거죠..

그런 거에요.

크기가 큰 배열에서 원소에 어떤 식으로 접근해 버리냐에 따라서 수행 시간이 차이가 나는 이유를 쉽게 설명하자면.. 이렇게 설명할 수 있어요.

궁금하시다면 data를 좀 크게 키워 놓고.

1000*1000짜리 행렬 곱셈을 한 번은 깡으로 해 보시고.

다른 한 번은 뒤에 오는 행렬을 Transpose 시켜서 곱해보세요. 차이가 확 날 거에요.


실제로 제 옛날 블로그에 측정한 자료가 있었는데요.

전치 행렬로 해서 곱해버린 경우에는 12초, 그렇지 않은 경우에는 1분 가까이 걸렸습니다.

chogahui05   6년 전

행우선, 열우선 방식은 많이 들어보셨을지도 모르겠는데요.

같이 찾아보시면 도움이 되실 듯 싶습니다.

댓글을 작성하려면 로그인해야 합니다.