알고리즘/algospot

algospot PINBALL

꾸준함. 2018. 3. 7. 22:24

문제 링크입니다: https://algospot.com/judge/problem/read/PINBALL

우선 문제를 간단하게 만들기 위해 공의 반지름이 1이기 때문에 공을 점으로 바꾸고 장애물의 반지름을 각각 1 증가시켰습니다.

공의 궤적을 시간 time에 대해 f(time)=here+time*dir로 표현했습니다. 여기서 here는 현 위치를 벡터로 표현한 것이고 dir은 공이 향하는 방향을 벡터로 표현한 것입니다.

중심이 center이고 반지름이 radius인 장애물과 교차하는 시간은 (center-f(t))*(center-f(t))=radius*radius로 표현할 수 있습니다.

위 이차방정식의 해를 근의 공식을 이용하여 구하였고 이를 통해 부딪히는 장애물 번호를 순차적으로 출력했습니다.


아래는 공이 장애물에 부딪힌 이후 움직일 각도를 계산하는 reflect 함수 추가설명입니다.

-dir은 들어오는 방향의 반댓방향입니다.

그리고 dir`는 교차하는 지점(contact)에 -dir을 사영한 결과입니다.

따라서 반사된 방향 dir``는 -dir에서 dir`로 가는 벡터를 두번 더한 결과입니다.


/*

2차원 벡터를 표현하는 vector2 클래스의 구현

*/

#include <iostream>

#include <vector>

#include <cmath>

using namespace std;

 

const double EPSILON = 1e-9;

const double INFTY = 1e200;

const double PI = 2.0*acos(0.0);

//2차원 벡터를 표현한다

 

struct vector2

{

        double x, y;

        //생성자를 explicit으로 지정하면 vector2를 넣을 곳에 잘못해

        //실수가 들어가는 일을 방지

        explicit vector2(double x_ = 0, double y_ = 0) :x(x_), y(y_)

        {

        }

        //두 벡터의 비교

        bool operator==(const vector2 &rhs) const

        {

               return x == rhs.x&&y == rhs.y;

        }

        bool operator<(const vector2 &rhs) const

        {

               return x != rhs.x ? x < rhs.x : y < rhs.y;

        }

        //벡터의 덧셈과 뺄셈

        vector2 operator+(const vector2 &rhs) const

        {

               return vector2(x + rhs.x, y + rhs.y);

        }

        vector2 operator-(const vector2 &rhs) const

        {

               return vector2(x - rhs.x, y - rhs.y);

        }

        //실수로 곱셈

        vector2 operator*(double rhs) const

        {

               return vector2(x*rhs, y*rhs);

        }

        //벡터의 길이를 반환

        double norm() const

        {

               return hypot(x, y);

        }

        //방향이 같은 단위 벡터를 반환

        //영벡터에 대해 호출한 경우 반환 값은 정의되지 않는다

        vector2 normalize() const

        {

               return vector2(x / norm(), y / norm());

        }

        //x축의 양의 방ㅎ향으로부터 이 벡터까지 반시계 방향으로 잰 각도

        double polar() const

        {

               return fmod(atan2(y, x) + 2 * PI, 2 * PI);

        }

        //내적/외적의 구현

        double dot(const vector2 &rhs) const

        {

               return x * rhs.x + y * rhs.y;

        }

        double cross(const vector2 &rhs) const

        {

               return x * rhs.y - rhs.x*y;

        }

        //이 벡터를 rhs에 사영한 결과

        vector2 project(const vector2 &rhs) const

        {

               vector2 r = rhs.normalize();

               return r * r.dot(*this);

        }

};

 

double howMuchCloser(vector2 p, vector2 a, vector2 b) //a b보다 얼마나 가까운가

{

        return (b - p).norm() - (a - p).norm();

}

 

//두 벡터의 방향성을 판단하는 ccw() 함수 구현

//원점에서 벡터 b가 벡터 a의 반시계 방향이면 양수, 시계 방향이면 음수

//평행이면 0을 반환

double ccw(vector2 a, vector2 b)

{

        return a.cross(b);

}

 

// p를 기준으로 벡터 b가 벡터 a의 반시계 방향이면 양수, 시계 방향이면 음수

//평행이면 0을 반환

double ccw(vector2 p, vector2 a, vector2 b)

{

        return ccw(a - p, b - p);

}

 

//두 직선의 교차점을 계산하는 liineIntersection() 함수의 구현

//(a, b)를 포함하는 선과 (c, d)를 포함하는 선의 교점을 x에 반환

//두 선이 평행이면 (겹치는 경우를 포함) 거짓을, 아니면 참을 반환

bool lineIntersection(vector2 a, vector2 b, vector2 c, vector2 d, vector2 &x)

{

        double det = (b - a).cross(d - c);

        //두 선이 평행인 경우

        if (fabs(det) < EPSILON)

               return false;

        x = a + (b - a)*((c - a).cross(d - c) / det);

        return true;

}

 

//(a, b) (c, d)가 평행한 두 선분일 때 이들이 한점에서 겹치는지 확인

bool parallelSegments(vector2 a, vector2 b, vector2 c, vector2 d, vector2 &p)

{

        if (b < a)

               swap(a, b);

        if (d < c)

               swap(c, d);

        //한 직선 위에 없거나 두 선분이 겹치지 않는 경우를 우선 걸러낸다

        if (ccw(a, b, c) != 0 || b < c || d < a)

               return false;

        //두 선분은 확실히 겹친다. 교차점을 하나 찾자

        if (a < c)

               p = c;

        else

               p = a;

        return true;

}

 

//p (a, b)를 감싸면서 각 변이 x, y축에 평행한 최소 사각형 내부에 있는지 확인

//a, b, p는 일직선 상에 있다고 가정

bool inBoundingRectangle(vector2 p, vector2 a, vector2 b)

{

        if (b < a)

               swap(a, b);

        return p == a || p == b || (a < p &&p < b);

}

 

//(a, b) 선분과 (c, d) 선분의 교점을 p에 반환

//교점이 여러개일 경우 아무 점이나 반환

//두 선분이 교차하지 않을 경우 false 반환

bool segmentIntersection(vector2 a, vector2 b, vector2 c, vector2 d, vector2 &p)

{

        //두 직선이 평행인 경우를 우선 예외 처리

        if (!lineIntersection(a, b, c, d, p))

               return parallelSegments(a, b, c, d, p);

        //p가 두 선분에 포함되어 있는 경우에만 참을 반환

        return inBoundingRectangle(p, a, b) && inBoundingRectangle(p, c, d);

}

 

//두 선분의 교차 여부를 좀 더 간단하게 확인

bool segmentIntersects(vector2 a, vector2 b, vector2 c, vector2 d)

{

        double ab = ccw(a, b, c)*ccw(a, b, d);

        double cd = ccw(c, d, a)*ccw(c, d, b);

        //두 선분이 한 직선 위에 있거나 끝점이 겹치는 경우

        if (ab == 0 && cd == 0)

        {

               if (b < a)

                       swap(a, b);

               if (d < c)

                       swap(c, d);

               return !(b < c || d < a);

        }

        return ab <= 0 && cd <= 0;

}

 

//점과 선 사이의 거리를 계산하는 함수 pointToLine()의 구현

// p에서 (a, b) 직선에 내린 수선의 발을 구한다

vector2 perpendicularFoot(vector2 p, vector2 a, vector2 b)

{

        return a + (p - a).project(b - a);

}

 

// p (a, b) 직선 사이의 거리를 구한다

double pointToLine(vector2 p, vector2 a, vector2 b)

{

        return (p - perpendicularFoot(p, a, b)).norm();

}

//여기까지가 vector2 관련 함수

//이후부터 PINBALL.cpp

 

int obstacleNum;

vector<vector2> center; //중심

vector<int> radius; //반지름

 

//2차 방정식 ax^2+bx+c=0의 모든 실근을 크기 순서대로 반환

vector<double> solve2(double a, double b, double c)

{

        double d = b * b - 4 * a*c;

        //해가 없는 경우

        if (d < -EPSILON)

               return vector<double>();

        //중근

        if (d < EPSILON)

               return vector<double>(1, -b / (2 * a));

        vector<double> result;

        result.push_back((-b - sqrt(d)) / (2 * a));

        result.push_back((-b + sqrt(d)) / (2 * a));

        return result;

}

 

//here에 있던 공이 1초마다 dir만큼 굴러갈 때, center를 중심으로 하고

//반지름 radius인 장애물과 몇 초 후에 충돌하는지 반환

//충돌하지 않을 경우 '아주 큰 값' INFTY를 반환

double hitCircle(vector2 here, vector2 dir, vector2 center, int radius)

{

        //f(t)=here+t*dir;

        //(center-f(t))*(center-f(t)) =

        //((dir*dir)*t^2) + (2*(here-center)*dir)*t + (center*center + here*here - 2*(center*here) - radius*radius)

        double a = dir.dot(dir);

        double b = 2 * dir.dot(here - center);

        double c = center.dot(center) + here.dot(here) - 2 * here.dot(center) - radius * radius;

 

        vector<double> sols = solve2(a, b, c);

        if (sols.empty() || sols[0] < EPSILON)

               return INFTY;

        return sols[0];

}

 

//here에 있던 공이 dir 방향으로 굴러와 center를 중심으로 하는 장애물에서

//contact 위치에서 충돌했을 때 공의 새로운 방향 반환

vector2 reflect(vector2 dir, vector2 center, vector2 contact)

{

        return (dir - dir.project(contact - center) * 2).normalize();

}

 

//공의 현재 위치와 방향이 주어질 때 최대 10번의 충돌 출력

void simulate(vector2 here, vector2 dir)

{

        //방향은 항상 단위 벡터로 저장

        dir = dir.normalize();

        int hitCount = 0;

        while (hitCount < 100)

        {

               //이번에 충돌한 장애물을 찾는다

               int circle = -1;

               double time = INFTY * 0.5;

               //각 장애물을 순회하며 가장 먼저 만나는 장애물을 찾는다

               for (int i = 0; i < obstacleNum; i++)

               {

                       double candidate = hitCircle(here, dir, center[i], radius[i] + 1);

                       if (candidate < time)

                       {

                              time = candidate;

                              circle = i;

                       }

               }

               //더 이상 장애물에 충돌하지 않고 게임판을 벗어나는 경우

               if (circle == -1)

                       break;

               //부딪히는 장애물의 번호를 출력

               if (hitCount++)

                       cout << " ";

               cout << circle;

               //공의 새 위치를 계산

               vector2 contact = here + dir * time;

               //부딪힌 위치와 새 방향으로 here dir 변경

               dir = reflect(dir, center[circle], contact);

               here = contact;

        }

        cout << endl;

}

 

int main(void)

{

        int test_case;

        cin >> test_case;

        if (test_case < 1 || test_case>50)

               exit(-1);

 

        for (int i = 0; i < test_case; i++)

        {

               center.clear();

               radius.clear();

               int x, y, dx, dy;

               cin >> x >> y >> dx >> dy >> obstacleNum;

               if (x < 0 || x>100 || y < 0 || y>100)

                       exit(-1);

               if (dx < -10 || dx>10 || dy < -10 || dy>10)

                       exit(-1);

 

               vector2 here(x, y);

               vector2 dir(dx, dy);

              

               for (int j = 0; j < obstacleNum; j++)

               {

                       int cx, cy;

                       cin >> cx >> cy;

                       center.push_back(vector2(cx, cy));

                       int r;

                       cin >> r;

                       radius.push_back(r);

               }

               simulate(here, dir);

        }

        return 0;

}


개발환경:Visual Studio 2017


지적, 조언, 질문 환영입니다! 댓글 남겨주세요~


[참고] 프로그래밍 대회에서 배우는 알고리즘 문제해결전략 

반응형

'알고리즘 > algospot' 카테고리의 다른 글

algospot NERDS  (2) 2018.03.18
algospot TREASURE  (0) 2018.03.11
algospot POTION  (0) 2018.03.04
algospot PASS486  (0) 2018.03.02
c++ 에라토스테네스의 체를 이용한 빠른 소인수 분해  (0) 2018.03.02