题目描述

龙曲线(Dragon Curve\texttt{Dragon Curve}Dragon Curve)是一种著名的分形图案。本题定义了一个二进制字符串序列 {sn}\{s_n\}{sn},其中 s0=1s_0 = 1s0=1,并且递推关系为:

sn=sn−1 1 r(sn−1) s_n = s_{n-1} \, 1 \, r(s_{n-1}) sn=sn11r(sn1)

这里 r(s)r(s)r(s) 表示将字符串 sss 反转后,再将所有 1 变为 0、所有 0 变为 1(即取反补码)。

机器人从原点出发,初始朝东,以每秒 111 个单位的速度匀速运动,并在每运动 111 秒后根据字符串 s10s_{10}s10 的第 kkk 个字符(kkk111 开始计数)决定转弯方向:若该字符为 1 则左转 90∘90^\circ90,否则右转 90∘90^\circ90。已知当输入为 s10s_{10}s10 时,机器人经过 210+1=20482^{10+1}=2048210+1=2048 秒后停在坐标 (−32,32)(-32, 32)(32,32)

现在将输入字符串改为 s30s_{30}s30(初始条件相同),机器人将移动 2312^{31}231 秒后停止。要求对于给定的任意时刻 nnn0≤n≤2310 \le n \le 2^{31}0n231),输出机器人从出发后经过 nnn 秒时的坐标。

输入格式

输入包含多组测试数据。每组数据占一行,包含一个非负整数 nnnn≤109n \le 10^9n109)。输入以负数(如 −1-11)作为结束标志。测试数据总数少于 500050005000 组。

输出格式

对于每个输入的 nnn,输出一行,格式为 (x,y),表示机器人经过 nnn 秒后的坐标。

样例

输入

1
2
3
2048
1000000000
-1

输出

(1,0)
(1,1)
(0,1)
(-32,32)
(9648,-31504)

题目分析

本题的核心是计算由分形字符串 s30s_{30}s30 所确定的路径上,前 nnn 个移动单位的总位移。

直接模拟 nnn 步是不可行的,因为 nnn 可达 10910^9109,而 s30s_{30}s30 的总长度是 2312^{31}231,远大于模拟上限。

观察递推式 sk=sk−1 1 r(sk−1)s_k = s_{k-1} \, 1 \, r(s_{k-1})sk=sk11r(sk1),可知字符串 sks_ksk 由三部分组成:

  1. 前半部分:sk−1s_{k-1}sk1,长度 2k2^k2k
  2. 中间一个字符 1,表示一次左转;
  3. 后半部分:r(sk−1)r(s_{k-1})r(sk1),长度也是 2k2^k2k

因此,整个路径也具有分形自相似结构:先走完 sk−1s_{k-1}sk1 对应的路径,然后左转 90∘90^\circ90,再走完 r(sk−1)r(s_{k-1})r(sk1) 对应的路径。关键在于,r(sk−1)r(s_{k-1})r(sk1) 所对应的路径与 sk−1s_{k-1}sk1 的路径之间存在确定的几何关系。

pkp_kpk 为执行完整字符串 sks_ksk 后的位移向量,dkd_kdk 为执行完后机器人的朝向向量(相对于初始方向)。由于机器人每次移动方向只能为东、北、西、南,我们可以用复平面上的单位向量表示方向。初始方向为东,即向量 (1,0)(1,0)(1,0)。左转相当于乘以虚数单位 iii,右转相当于乘以 −i-ii

对于 r(sk−1)r(s_{k-1})r(sk1),它的路径是 sk−1s_{k-1}sk1 路径的“反向”版本:首先反转顺序,然后每个转向取反(左变右,右变左)。这个变换可以用向量运算描述。若已知 pk−1p_{k-1}pk1dk−1d_{k-1}dk1,则 r(sk−1)r(s_{k-1})r(sk1) 的位移 qqq 和最终方向 eee 满足:

  • 因为取反使所有转弯方向反转,所以最终方向 e=−dk−1e = -d_{k-1}e=dk1(相对于初始方向);
  • 位移 qqq 满足 q=(1,0)−dk−1⋅pk−1+dk−12q = (1,0) - d_{k-1} \cdot p_{k-1} + d_{k-1}^2q=(1,0)dk1pk1+dk12,其中 dk−12d_{k-1}^2dk12 表示方向向量旋转 180∘180^\circ180,即 (−1,0)(-1,0)(1,0) 方向的向量。

利用这些关系,我们可以递归地计算任意前缀的位移。

解题思路

向量表示与运算

用结构体 Vec 表示二维向量,重载加法、减法、乘法(复数乘法,用于旋转)。复数乘法规则:(a+bi)(c+di)=(ac−bd)+(ad+bc)i(a+bi)(c+di) = (ac - bd) + (ad + bc)i(a+bi)(c+di)=(acbd)+(ad+bc)i。在二维坐标中,向量 (x,y)(x,y)(x,y) 对应复数 x+yix + yix+yi

初始方向向量为 (1,0)(1,0)(1,0),左转 90∘90^\circ90 就是乘以 (0,1)(0,1)(0,1),右转 90∘90^\circ90 就是乘以 (0,−1)(0,-1)(0,1)

预计算完整字符串的信息

对于 k=0k = 0k=0,有 s0=1s_0 = 1s0=1,即一个字符 1 表示左转。机器人先向前走 111 秒(位移 (1,0)(1,0)(1,0)),然后左转,故 p0=(1,0)+(0,1)=(1,1)p_0 = (1,0) + (0,1) = (1,1)p0=(1,0)+(0,1)=(1,1),最终方向 d0=(0,1)d_0 = (0,1)d0=(0,1)

对于 k≥1k \ge 1k1,根据递推关系:

  • 先执行 sk−1s_{k-1}sk1,位移为 pk−1p_{k-1}pk1,方向变为 dk−1d_{k-1}dk1
  • 中间字符 1 使方向左转,得到方向 d′=dk−1⋅(0,1)d' = d_{k-1} \cdot (0,1)d=dk1(0,1)
  • 再执行 r(sk−1)r(s_{k-1})r(sk1),其位移相对于当前方向 d′d'dqqq(下面推导),最终方向为 d′⋅ed' \cdot ede,其中 e=−dk−1e = -d_{k-1}e=dk1

计算 qqqr(sk−1)r(s_{k-1})r(sk1) 的位移相对于执行 sk−1s_{k-1}sk1 的初始方向而言,可以表示为 q=(1,0)−dk−1⋅pk−1+dk−12q = (1,0) - d_{k-1} \cdot p_{k-1} + d_{k-1}^2q=(1,0)dk1pk1+dk12。这里 dk−12d_{k-1}^2dk12 表示方向 dk−1d_{k-1}dk1 旋转 180∘180^\circ180 后的向量。

推导思路:r(s)r(s)r(s) 的路径等价于从原点出发,先沿 sss 的终点方向相反方向走一段,然后转向,最后到达一个位置。具体推导可借助复数几何,但本题解中直接使用上述公式。

因此,预计算数组 fullPos[k]fullDir[k],分别表示执行完整 sks_ksk 的位移和最终方向(均相对于初始方向)。递推时注意,后半部分的位移需要先乘以当前方向 d′d'd 进行旋转,因为 qqq 是相对于 sk−1s_{k-1}sk1 的初始方向的。

任意前缀的查询

对于给定的 nnn,我们要求 s30s_{30}s30nnn 个字符对应的位移。定义一个递归函数 getPrefix(k, n, dir),表示在初始方向为 dir 的情况下,执行 sks_ksk 的前 nnn 个移动步(即前 nnn 个字符)后的位移和最终方向。

递归过程:

  • n=0n = 0n=0,返回零位移和 dir
  • k=0k = 0k=0,直接模拟两个字符(因为 s0s_0s0 长度为 222,但 nnn 可能为 111222)。
  • half=2khalf = 2^khalf=2k(即 sk−1s_{k-1}sk1 的长度)。若 n≤halfn \le halfnhalf,则直接递归到 getPrefix(k-1, n, dir)
  • 否则,先走完 sk−1s_{k-1}sk1,得到位移 pos1pos_1pos1 和方向 dir1dir_1dir1;然后中间一个左转(因为字符是 1),方向变为 dir2=dir1⋅(0,1)dir_2 = dir_1 \cdot (0,1)dir2=dir1(0,1);剩余步数为 n−halfn - halfnhalf,需要执行 r(sk−1)r(s_{k-1})r(sk1) 的前 n−halfn - halfnhalf 步。由于 r(sk−1)r(s_{k-1})r(sk1)sk−1s_{k-1}sk1 结构类似但转向相反,我们定义另一个函数 getRevPrefix(k-1, r, dir) 来计算 r(sk−1)r(s_{k-1})r(sk1) 的前缀。

getRevPrefix 的递归逻辑类似,但是中间字符为右转,并且其前半部分仍是 sk−1s_{k-1}sk1(因为 r(sk−1)r(s_{k-1})r(sk1) 的结构为 r(sk−1)=r(sk−1)r(s_{k-1}) = r(s_{k-1})r(sk1)=r(sk1)?实际上,r(sk)=r(sk−1) 0 sk−1r(s_{k}) = r(s_{k-1}) \, 0 \, s_{k-1}r(sk)=r(sk1)0sk1,因为 r(ab)=r(b)r(a)r(ab) = r(b) r(a)r(ab)=r(b)r(a) 且取反交换。所以 r(sk)=r(sk−1) 0 sk−1r(s_k) = r(s_{k-1}) \, 0 \, s_{k-1}r(sk)=r(sk1)0sk1。因此,r(sk)r(s_k)r(sk) 的前半部分是 r(sk−1)r(s_{k-1})r(sk1),中间是 0(右转),后半部分是 sk−1s_{k-1}sk1。这导致递归中,对于 r(sk)r(s_k)r(sk) 的前缀,若 n≤halfn \le halfnhalf 则调用 getRevPrefix(k-1, n, dir),否则走完前半部分后中间右转,再调用 getPrefix(k-1, ...)

通过这样的递归,每次 kkk 减小 111,因此查询复杂度为 O(30)O(30)O(30),完全可接受。

坐标变换与方向旋转

在递归过程中,每一步的位移都需要乘以当前的方向向量(因为位移是相对于当前方向的)。我们利用复数乘法实现旋转。

复杂度分析

  • 预计算:O(31)O(31)O(31) 次循环,每次常数运算。
  • 每次查询:递归深度最多 303030 层,每层常数运算,因此 O(1)O(1)O(1)(常数约 303030)。
  • 总时间复杂度:O(T⋅30)O(T \cdot 30)O(T30),其中 T≤5000T \le 5000T5000,非常高效。
  • 空间复杂度:O(1)O(1)O(1)(数组大小固定)。

代码实现

// Fractal
// UVa ID: 11253
// Verdict: Accepted
// Submission Date: 2026-06-21
// UVa Run Time: 0.000s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

struct Vec {
    ll x, y;
};

Vec addVec(const Vec& a, const Vec& b) { return {a.x + b.x, a.y + b.y}; }
Vec subVec(const Vec& a, const Vec& b) { return {a.x - b.x, a.y - b.y}; }
Vec mulVec(const Vec& a, const Vec& b) { return {a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x}; }

Vec fullPos[31], fullDir[31]; // fullPos[k] = 完整 s_k 的位移,fullDir[k] = 最终方向

struct Result {
    Vec pos; // 位移
    Vec dir; // 最终方向
};

// 前向声明
Result getPrefix(int k, ll n, const Vec& dir);
Result getRevPrefix(int k, ll n, const Vec& dir);

// 计算 s_k 的前 n 步(移动段)
Result getPrefix(int k, ll n, const Vec& dir) {
    if (n == 0) return {{0, 0}, dir};
    if (k == 0) {
        Vec step1 = dir;
        Vec turn1 = mulVec(dir, {0, 1}); // 左转
        if (n == 1) return {step1, turn1};
        Vec step2 = turn1;
        Vec pos = addVec(step1, step2);
        return {pos, turn1};
    }
    ll half = 1LL << k; // s_{k-1} 的移动步数
    if (n <= half) return getPrefix(k - 1, n, dir);
    // 走完前半部分 s_{k-1}
    Vec pos1 = mulVec(dir, fullPos[k - 1]);
    Vec dir1 = mulVec(dir, fullDir[k - 1]);
    // 中间左转
    Vec dir2 = mulVec(dir1, {0, 1});
    ll r = n - half;
    Result part3 = getRevPrefix(k - 1, r, dir2); // 第三部分 r(s_{k-1})
    Vec totalPos = addVec(pos1, part3.pos);
    return {totalPos, part3.dir};
}

// 计算 r(s_k) 的前 n 步(移动段)
Result getRevPrefix(int k, ll n, const Vec& dir) {
    if (n == 0) return {{0, 0}, dir};
    if (k == 0) {
        Vec step1 = dir;
        Vec turn1 = mulVec(dir, {0, -1}); // 右转
        if (n == 1) return {step1, turn1};
        Vec step2 = turn1;
        Vec pos = addVec(step1, step2);
        return {pos, turn1};
    }
    ll half = 1LL << k;
    if (n <= half) return getPrefix(k - 1, n, dir); // 前半部分也是 s_{k-1}
    Vec pos1 = mulVec(dir, fullPos[k - 1]);
    Vec dir1 = mulVec(dir, fullDir[k - 1]);
    // 中间右转
    Vec dir2 = mulVec(dir1, {0, -1});
    ll r = n - half;
    Result part3 = getRevPrefix(k - 1, r, dir2); // 第三部分 r(s_{k-1})
    Vec totalPos = addVec(pos1, part3.pos);
    return {totalPos, part3.dir};
}

int main() {
    // 预计算完整 s_k 的位移和最终方向
    fullPos[0] = {1, 1};
    fullDir[0] = {0, 1};
    for (int k = 1; k <= 30; ++k) {
        Vec p = fullPos[k - 1], d = fullDir[k - 1];
        // r(s_{k-1}) 的完整位移 Q = 1 - d*p + d^2
        Vec q = subVec({1, 0}, mulVec(d, p));
        q = addVec(q, mulVec(d, d));
        // r(s_{k-1}) 的最终方向 R = -d
        Vec rDir = {-d.x, -d.y};
        // 左转
        Vec left = mulVec(d, {0, 1});
        fullPos[k] = addVec(p, mulVec(left, q));
        fullDir[k] = mulVec(left, rDir);
    }

    ll n;
    while (cin >> n && n >= 0) {
        Result ans = getPrefix(30, n, {1, 0});
        cout << "(" << ans.pos.x << "," << ans.pos.y << ")\n";
    }
    return 0;
}

总结

本题的关键在于识别龙曲线的分形递归结构,并利用向量运算(复数旋转)将路径的几何变换与字符串的递归定义紧密结合。通过预计算完整子串的位移和方向,以及对任意前缀进行分治递归,我们能够在常数时间内回答每次查询,从而高效解决大规模数据。

  • 主要技巧:分形自相似性 + 向量旋转 + 递归降维。
  • 注意点:r(s)r(s)r(s) 的位移公式需要仔细推导,避免方向错误。
  • 复杂度极低,适合处理 10910^9109 级别的时间步长和多组测试数据。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐