华为OD 经典屏保

1. 题意

DVD 机在视频输出时,为了保护电视显像管,在待机状态会显示 “屏保动画”,DVD Logo 在屏幕内来回运动,碰到边缘会反弹。

请根据如下要求,实现屏保 Logo 坐标的计算算法:

屏幕与坐标系:屏幕是 800×600 像素的矩形,左上角为坐标原点,横边向右为 X 轴,竖边向下为 Y 轴。
Logo 初始状态:Logo 是 50×25 像素的矩形,初始左上角坐标为 (x, y),在 X、Y 方向均以 1 像素 / 秒的速度运动。
边缘反弹:碰到屏幕四个边缘(左、右、上、下)时,发生镜面反弹(以 45° 碰撞并反弹)。
角落碰撞:碰到屏幕四个角时,因两个边缘同时反弹,Logo 会原路返回。

2. 题解

由于小球在两个方向上的运动是独立的,因此我们可以分方向进行计算。

总体代码

#include <iostream>
#include <string>
#include <vector>

using namespace std;
static constexpr int WIND_W = 800;
static constexpr int WIND_H = 600;

static constexpr int REC_W = 50;
static constexpr int REC_H = 25;

static constexpr int MXX = WIND_W - REC_W;
static constexpr int MXY = WIND_H - REC_H;


int vx = 1;
int vy = 1;
int dx = 1;
int dy = 1;

int x;
int y;
int t;
void solve1()
{
    int cx = x;
    int cy = y;

    for (int i = 0;i < t; ++i) {
        int nx_l = cx + dx * vx;
        int nx_r = cx + REC_W - 1 + dx * vx;

        // nx_r = nx_l + REC_W - 1
        // nx_r <= WIND_W - 1 <==> nx_l < WIND_W - REC_W
        if ( nx_l < 0 || nx_r >= WIND_W) {
            dx *= -1;
        }
        
        // ny_b = ny_u + REC_H - 1
        // ny_b <= WIND_H - 1 <==> ny_u <= WIND_H - REC_H
        int ny_u = cy + dy * vy;
        int ny_b = cy + REC_H - 1 + dy * vy;
        if ( ny_u < 0 || ny_b >= WIND_H) {
            dy *= -1;
        }

        cx += dx * vx;
        cy += dy * vy;
    }

    cout << cx << " " << cy << endl;
}

void solve1_1()
{
    int cx = x;
    int cy = y;
    for (int i = 0; i < t; ++i) {
        int nx = cx + dx ;
        int ny = cy + dy ;

        if ( nx < 0 || nx > MXX ) {
            dx *= -1;
        }
        if ( ny < 0 || ny > MXY ) {
            dy *= -1;
        }
        cx += dx;
        cy += dy;
    }

    cout << cx << " " << cy << endl;
}



void solve2()
{
    vector<int> posx;
    vector<int> posy;

    
    int Tx = 2 * (WIND_W - REC_W);
    int Ty = 2 * (WIND_H - REC_H);

    int cx = x;
    int cy = y;

    int inc = 1;
    for (int i = 0; i < Tx; ++i) {
       posx.push_back(cx);
       if (cx == MXX) {
            inc = -1;
       }
       else if ( cx == 0) {
            inc = 1;
       }
       cx += inc;
    }

    inc = 1;
    for (int i = 0; i < Ty; ++i) {
        posy.push_back( cy );
        if ( cy == MXY) {
            inc = -1;
        }
        else if ( cy == 0) {
            inc = 1;
        }
        cy += inc;
    }

    cout << posx[ t % Tx ] << " " << posy[ t % Ty ] << endl;
}

void solve3()
{
    auto getPos = [](int start, int t, int MX, int T)->int {
        t %= T;

        if (t <= MX - start) {
            return start + t;
        }
        else {
            return MX - (t - (MX - start));
        }
    };

    cout << getPos( x, t, MXX, 2 * MXX) << " " << getPos( y, t, MXY, 2 * MXY) << endl;
}

int main()
{

    cin >> x >> y >> t;
    
    solve1_1();
    

    return 0;
}
2.1 直接模拟

根据移动的方向来计算下一秒的位置,由于矩形logo是矩形的,因此我们需要判断下一秒四个矩形边界是否超出了屏幕的范围。

static constexpr int WIND_W = 800;
static constexpr int WIND_H = 600;

static constexpr int REC_W = 50;
static constexpr int REC_H = 25;

static constexpr int MXX = WIND_W - REC_W;
static constexpr int MXY = WIND_H - REC_H;


int vx = 1;
int vy = 1;
int dx = 1;
int dy = 1;

int x;
int y;
int t;
void solve1()
{
    int cx = x;
    int cy = y;

    for (int i = 0;i < t; ++i) {
        int nx_l = cx + dx * vx;
        int nx_r = cx + REC_W - 1 + dx * vx;

        // nx_r = nx_l + REC_W - 1
        // nx_r <= WIND_W - 1 <==> nx_l < WIND_W - REC_W
        if ( nx_l < 0 || nx_r >= WIND_W) {
            dx *= -1;
        }
        
        // ny_b = ny_u + REC_H - 1
        // ny_b <= WIND_H - 1 <==> ny_u <= WIND_H - REC_H
        int ny_u = cy + dy * vy;
        int ny_b = cy + REC_H - 1 + dy * vy;
        if ( ny_u < 0 || ny_b >= WIND_H) {
            dy *= -1;
        }

        cx += dx * vx;
        cy += dy * vy;
    }

    cout << cx << " " << cy << endl;
}

事实上我们可以根据左边界和logo的宽度来计算右边界,同理可以根据上边界的位置和高度计算logo下边界的位置。因此我们可以稍微简化一下代码。

xl+logo_w−1=xrxr≤wind_w−1xl≤wind_w−logo_wyu+logo_h−1=ybyb≤wind_h−1yu≤wind_h−logo_h x_l+logo\_w-1 =x_r\\ x_r \le wind\_w-1\\ x_l \le wind\_w-logo\_w\\ y_u+logo\_h -1=y_b\\ y_b\le wind\_h-1\\ y_u \le wind\_h-logo\_h xl+logo_w1=xrxrwind_w1xlwind_wlogo_wyu+logo_h1=ybybwind_h1yuwind_hlogo_h
我们令MXX=wind_w−logo_wMXX=wind\_w-logo\_wMXX=wind_wlogo_w, MXY=wind_h−logo_hMXY=wind\_h-logo\_hMXY=wind_hlogo_h

void solve1_1()
{
    int cx = x;
    int cy = y;
    for (int i = 0; i < t; ++i) {
        int nx = cx + dx ;
        int ny = cy + dy ;

        if ( nx < 0 || nx > MXX ) {
            dx *= -1;
        }
        if ( ny < 0 || ny > MXY ) {
            dy *= -1;
        }
        cx += dx;
        cy += dy;
    }

    cout << cx << " " << cy << endl;
}

时间复杂度O(t)O(t)O(t)
空间复杂度O(1)O(1)O(1)

2.2 预处理周期

我们其实可以观察到,小球在两个方向的运动都是周期性运动的。

因此我们可以预处理出一个周期的所有位置可能,最后再来取答案就可以了。

那么周期是多少呢?

不难发现小球的左上位置实际上是从

0→MX→0 0\rightarrow MX \rightarrow0 0MX0
这样循环往复的,因此周期是2MX2MX2MX

void solve2()
{
    vector<int> posx;
    vector<int> posy;

    
    int Tx = 2 * (WIND_W - REC_W);
    int Ty = 2 * (WIND_H - REC_H);

    int cx = x;
    int cy = y;

    int inc = 1;
    for (int i = 0; i < Tx; ++i) {
       posx.push_back(cx);
       if (cx == MXX) {
            inc = -1;
       }
       else if ( cx == 0) {
            inc = 1;
       }
       cx += inc;
    }

    inc = 1;
    for (int i = 0; i < Ty; ++i) {
        posy.push_back( cy );
        if ( cy == MXY) {
            inc = -1;
        }
        else if ( cy == 0) {
            inc = 1;
        }
        cy += inc;
    }

    cout << posx[ t % Tx ] << " " << posy[ t % Ty ] << endl;
}

时间复杂度O(1)O(1)O(1)
空间复杂度O(Tx+Ty)O(Tx+Ty)O(Tx+Ty)

2.3 直接计算

事实上知道周期后,我们可以直接计算,因为初始的速度一定是增,我们只需要看时间对周期取余后,是否能到达最大的位置。能到达就再处理下往回走。

void solve3()
{
    auto getPos = [](int start, int t, int MX, int T)->int {
        t %= T;

        if (t <= MX - start) {
            return start + t;
        }
        else {
            return MX - (t - (MX - start));
        }
    };

    cout << getPos( x, t, MXX, 2 * MXX) << " " << getPos( y, t, MXY, 2 * MXY) << endl;
}

时间复杂度O(1)O(1)O(1)
空间复杂度O(1)O(1)O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值