题目大意
题目描述
构造一个长度不超过 500 500 5 0 0 且仅由大写字母 C、G、A、T 组成的非空字符串,使其满足以下两个条件:
字符串中包含恰好 G G G 个子序列 GATA(即字符 G、A、T、A 按顺序出现,中间可以包含其他字符)。
字符串中包含恰好 C C C 个子序列 CAT(即字符 C、A、T 按顺序出现,中间可以包含其他字符)。
输入格式
第一行包含一个整数 Q Q Q (1 ≤ Q ≤ 1000 1 \le Q \le 1000 1 ≤ Q ≤ 1 0 0 0 ),表示询问的组数。
接下来的 Q Q Q 行,每行包含两个整数 G G G 和 C C C (0 ≤ G , C ≤ 1 0 6 0 \le G, C \le 10^6 0 ≤ G , C ≤ 1 0 6 ),分别表示所需的 GATA 子序列的数量和 CAT 子序列的数量。
输出格式
对于每组询问,输出一行满足条件的字符串(长度不超过 500 500 5 0 0 且仅包含 C、G、A、T)。如果存在多个解,输出任意一个即可。题目保证在给定限制下一定有解。
样例输入与输出
样例 1:
1 2 3 4 5 6 7 8 9 10 11 12 Input: 4 1 1 2 3 18 1 2 1 Output: GATCAT GGCATAT GATGGATATCAT GGATCAT
样例 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Input: 5 1 0 0 1 1 1 0 0 0 0 Output: GATAT CAT GATCAT GG GG
样例解释
对于样例 1 的第一组询问(G = 1 , C = 1 G=1, C=1 G = 1 , C = 1 ),输出为 GATCAT:
对于样例 2 的第一组询问(G = 1 , C = 0 G=1, C=0 G = 1 , C = 0 ),输出为 GATAT:
思路讲解
gemini 的固定骨架式插入构造方法
题目描述
需要构造一个长度不超过 500 500 5 0 0 且仅由大写字母 C、G、A、T 组成的非空字符串,要求该字符串中包含恰好 G G G 个 GATA 子序列和恰好 C C C 个 CAT 子序列。
输入包含多组询问(Q ≤ 1000 Q \le 1000 Q ≤ 1 0 0 0 ),每组询问给出 G G G 和 C C C (0 ≤ G , C ≤ 1 0 6 0 \le G, C \le 10^6 0 ≤ G , C ≤ 1 0 6 )。
解题思路
我们可以采用“固定骨架 + 贪心插入字符 ”的策略。
首先,我们构造一个由 A 和 T 交替组成的骨架字符串:( A T ) K = ATAT … AT (AT)^K = \text{ATAT}\dots\text{AT} ( A T ) K = ATAT … AT (共 K K K 对)。
因为目标子序列 CAT 和 GATA 分别以唯一的字符 C 和 G 开头,且在我们的构造中,C 和 G 只会作为这两个子序列的起始字符,所以它们的贡献是完全独立 且互不干扰的。
当我们在这个骨架中插入一个 C 时,它能形成的 CAT 数量仅取决于它右侧的 A 和 T。
如果我们把 C 插入到倒数第 i i i 个 A 的前面(即其右侧恰好有 i i i 对 AT),那么它右侧的骨架字符串本质上是 ( A T ) i (AT)^i ( A T ) i 。
经过数学推导,在 ( A T ) i (AT)^i ( A T ) i 中:
AT 子序列的数量为:AT i = i × ( i + 1 ) 2 \text{AT}_i = \frac{i \times (i+1)}{2} AT i = 2 i × ( i + 1 )
ATA 子序列的数量为:ATA i = ( i − 1 ) × i × ( i + 1 ) 6 \text{ATA}_i = \frac{(i-1) \times i \times (i+1)}{6} ATA i = 6 ( i − 1 ) × i × ( i + 1 )
因此,在倒数第 i i i 个 A 前面插入一个 C,就会增加 AT i \text{AT}_i AT i 个 CAT;同理,插入一个 G,就会增加 ATA i \text{ATA}_i ATA i 个 GATA。
为了用最少的字符凑出恰好 C C C 个 CAT 和 G G G 个 GATA,我们可以将其转化为完全背包问题(找零钱问题) 。由于 AT i \text{AT}_i AT i 是三角形数,ATA i \text{ATA}_i ATA i 是四面体数,它们的增长速度非常快,使用贪心算法 (每次优先分配面值最大的数)可以极快地完成任务,且所需的“硬币”数量极少。
经过计算,当选定 K = 130 K = 130 K = 1 3 0 时:
骨架长度为 260 260 2 6 0 。
最大可用面值 AT ∗ 130 = 8515 \text{AT}*{130} = 8515 AT ∗ 1 3 0 = 8 5 1 5 ,ATA ∗ 130 = 366145 \text{ATA}*{130} = 366145 ATA ∗ 1 3 0 = 3 6 6 1 4 5 。
凑出最大的 1 0 6 10^6 1 0 6 时,最多只需要约 120 120 1 2 0 个 C 和约 10 10 1 0 个 G。
总长度最大约为 260 + 120 + 10 = 390 260 + 120 + 10 = 390 2 6 0 + 1 2 0 + 1 0 = 3 9 0 ,严格满足题目中要求的不超过 500 500 5 0 0 字符的限制。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> #include <string> #include <vector> using namespace std;long long AT[135 ];long long ATA[135 ];void precompute () { for (int i = 1 ; i <= 130 ; i++) { AT[i] = (long long )i * (i + 1 ) / 2 ; ATA[i] = (long long )(i - 1 ) * i * (i + 1 ) / 6 ; } } void solve () { long long G, C; cin >> G >> C; vector<int > cnt_C (135 , 0 ) ; vector<int > cnt_G (135 , 0 ) ; long long rem_C = C; for (int i = 130 ; i >= 1 ; i--) { if (AT[i] > 0 ) { cnt_C[i] = rem_C / AT[i]; rem_C %= AT[i]; } } long long rem_G = G; for (int i = 130 ; i >= 1 ; i--) { if (ATA[i] > 0 ) { cnt_G[i] = rem_G / ATA[i]; rem_G %= ATA[i]; } } string res = "" ; for (int i = 130 ; i >= 1 ; i--) { res.append (cnt_C[i], 'C' ); res.append (cnt_G[i], 'G' ); res += "AT" ; } cout << res << "\\n" ; } int main () { ios_base::sync_with_stdio (false ); cin.tie (NULL ); precompute (); int Q; if (cin >> Q) { while (Q--) { solve (); } } return 0 ; }
复杂度分析与代码解释
时间复杂度 :
预处理的时间复杂度为 O ( K ) O(K) O ( K ) 。对于每组询问,贪心求解两个面值数组并拼装字符串的操作最多只需要遍历 K K K 次,循环内的除法与取模均为常数时间操作,因此单次查询的时间复杂度为 O ( K ) O(K) O ( K ) 。
总时间复杂度为 O ( K + Q × K ) O(K + Q \times K) O ( K + Q × K ) ,其中 K = 130 , Q ≤ 1000 K = 130, Q \le 1000 K = 1 3 0 , Q ≤ 1 0 0 0 ,总体运算量不到 2 × 1 0 5 2 \times 10^5 2 × 1 0 5 次,能够在一瞬间通过 1000 1000 1 0 0 0 组询问。
空间复杂度 :
我们只用了一维数组存储 130 130 1 3 0 个计算好的面额,和输出使用的字符串等局部变量,空间复杂度为 O ( K ) O(K) O ( K ) 。极低的内存占用,安全可靠。
特殊边界考虑 :
当 G = 0 , C = 0 G = 0, C = 0 G = 0 , C = 0 时,所有的余数分配都会置零。最终程序只会打印单纯由 ( A T ) 130 (AT)^{130} ( A T ) 1 3 0 构成的骨架,此时没有 C 也没有 G 混入其中,答案为合法字符串,长 260 260 2 6 0 ,完美符合题意。最大要求情况下,贪心的结果余数迅速收敛下降,确保最终总长度不会越过 500 500 5 0 0 限制。
我确实也是想到了插入式的这个构造方法,但是确实没他想的这个么全。
不难发现如下规律:
我采用了过程式编程,模块化的思想,把不同的功能模块拆成了不同的函数,避免了Spaghetti Code,也就是面条代码。
我了解了一下,这个不算函数式编程,函数式编程的要求非常严格。
函数式编程的定义,以及我的代码为什么不是函数式编程(函数式编程中甚至不能使用 while 和 for 循环,用递归函数代替,有点变态了)(函数式编程最核心的思想是,就是声明的所有变量,全部都是 const 类型,即不可变)
1. 什么是函数式编程(Functional Programming, FP)?
函数式编程 是一种编程范式(也就是一种编写代码的思想和风格),它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象 。
它的几个核心特点包括:
纯函数(Pure Functions) :给定相同的输入,永远返回相同的输出,并且没有任何“副作用”(Side Effects)。副作用是指修改了函数外部的状态,比如修改全局变量、读写文件、控制台输出等。
不可变性(Immutability) :变量一旦被创建,就不能再被修改。如果你需要改变数据,你不能修改原数据,而是要创建一个包含新数据的新副本。
避免共享状态(Avoid Shared State) :函数之间不通过共享的全局变量来传递状态,所有需要的数据都通过参数传递。
声明式编程(Declarative) :关注“做什么”(What to do),而不是“怎么做”(How to do it)。通常会大量使用 map, reduce, filter 等高阶函数,而不是使用 for 或 while 循环一步步改变状态。
2. 你的代码算函数式编程吗?
不算。 你的代码属于典型的指令式编程(Imperative Programming)和过程式编程(Procedural Programming) ,这也是 C++ 在算法竞赛(CP)中最常见的标准写法。
你的代码中有几个明显违背 函数式编程原则的地方:
违背“无副作用”和“避免共享状态”
在你的代码中,存在全局变量:
1 2 vector<ll> at_ls, ata_ls; ll lT, testcase;
并且你的 init() 函数直接修改了这些外部的全局变量:
1 2 3 4 5 void init () { at_ls.resize (siz_AT_repeat); ata_ls.resize (siz_AT_repeat); }
在严格的函数式编程中,函数不应该修改外部环境。init 应该返回初始化好的数组,而不是修改全局变量。
违背“不可变性(Immutability)”
在 opls_insert 函数中,你使用了引用传递 &ans 来就地修改 数据:
1 2 3 4 void opls_insert (vector<Ch_Num_CG> &ans, vector<ll> opls, ll offset, char cg) { ans[i - offset].num_g++; }
在函数式编程中,输入参数是不允许被修改的。标准的做法应该是把 ans 传进去,然后在函数内部生成一个新的 vector<Ch_Num_CG> 并返回。
大量使用指令式的循环状态修改
你的代码大量使用了 for 和 while 循环来一步步改变局部变量的状态。例如在 greedy_cal 中:
1 2 3 4 while (x != 0 ) { x -= *it; }
在函数式编程中,这种逻辑通常会用**递归(Recursion)**或者折叠函数(如 std::accumulate / reduce)来实现,以避免变量状态的不断变更。
总结
你的代码中确实用到了 std::partial_sum, std::upper_bound, std::sort 这些来自 <algorithm> 库的函数,它们带有一点“声明式”的味道,但整体架构上你的代码依然是基于状态突变(State Mutation)的过程式编程。
对于算法竞赛而言,你现在的写法(过程式/指令式)是绝对正确且最高效 的。真正的函数式编程(如 Haskell 代码,或者在 C++ 中全程不使用非 const 变量)会带来极大的内存开销和常数级别的性能损耗,这在打 Codeforces 这种对时空常数要求极高的比赛中通常是不讨好的。
那么我这个代码更算是什么呢?
逐步求精(Stepwise Refinement)
这是由计算机科学家尼克劳斯·维尔特(Niklaus Wirth,PASCAL 语言的发明者)在 1971 年提出的。指的是将一个复杂的任务,不断分解成更小、更简单的子任务,直到这些子任务简单到可以用几行代码直接实现为止****。
我相信这个代码还是很清楚的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 vector<ll> at_ls, ata_ls; ll siz_AT_repeat = 140 ; void init () { at_ls.resize (siz_AT_repeat); ata_ls.resize (siz_AT_repeat); for (int i = 1 ; i < siz_AT_repeat; ++i) { at_ls[i] = i; ata_ls[i] = i; } partial_sum (all (at_ls), at_ls.begin ()); partial_sum (all (ata_ls), ata_ls.begin ()); partial_sum (all (ata_ls), ata_ls.begin ()); #ifdef LOCAL debug1d (ata_ls); debug1d (at_ls); #endif } vector<ll> greedy_cal (const vector<ll> &A, ll x) { vector<ll> res; while (x != 0 ) { auto it = upper_bound (all (A), x); if (it == A.begin ()) { assert (0 ); } else { --it; res.push_back (it - A.begin ()); x -= *it; } } #ifdef LOCAL debug1d (res); #endif return res; } struct Ch_Num_CG { char ch; ll num_g; ll num_c; }; void opls_insert (vector<Ch_Num_CG> &ans, vector<ll> opls, ll offset, char cg) { sort (all (opls), greater ()); ll cnta = 0 ; for (int i = SZ (ans) - 1 ; i > offset; --i) { if (ans[i].ch == 'A' ) { cnta++; } while (!opls.empty () && opls.back () == cnta) { if (cg == 'G' ) { ans[i - offset].num_g++; } else { ans[i - offset].num_c++; } opls.pop_back (); } } } string gen_ans (const vector<Ch_Num_CG> &ans) { string res; for (auto [ch,num_g,num_c]: ans) { res += string (num_g, 'G' ); res += string (num_c, 'C' ); res += ch; } return res; } void Solve () { ll G, C; cin >> G >> C; vector<ll> g_opls = greedy_cal (ata_ls, G); vector<ll> c_opls = greedy_cal (at_ls, C); string s; vector<Ch_Num_CG> Ans; for (int i = 0 ; i < siz_AT_repeat + 5 ; ++i) { Ans.push_back ({'A' , 0 , 0 }); Ans.push_back ({'T' , 0 , 0 }); } opls_insert (Ans,g_opls,2 ,'G' ); opls_insert (Ans,c_opls,0 ,'C' ); cout << gen_ans (Ans) << "\n" ; }
AC代码
AC
https://codeforces.com/gym/106416/submission/368168341
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 #include <bits/stdc++.h> #define all(vec) vec.begin(),vec.end() #define lson(o) (o<<1) #define rson(o) (o<<1|1) #define SZ(a) ((long long) a.size()) #define debug(var) cerr << #var <<" = [" <<var<<"]" <<"\n" ; #define debug1d(a) \ cerr << #a << " = [" ; \ for (int i = 0; i < (int)(a).size(); i++) \ cerr << (i ? ", " : "" ) << a[i]; \ cerr << "]\n" ; #define debug2d(a) \ cerr << #a << " = [\n" ; \ for (int i = 0; i < (int)(a).size(); i++) \ { \ cerr << " [" ; \ for (int j = 0; j < (int)(a[i]).size(); j++) \ cerr << (j ? ", " : "" ) << a[i][j]; \ cerr << "]\n" ; \ } \ cerr << "]\n" ; #define cend cerr<<"\n-----------\n" #define fsp(x) fixed<<setprecision(x) using namespace std;using ll = long long ;using ull = unsigned long long ;using DB = double ;using i128 = __int128;using CD = complex<double >;static constexpr ll MAXN = (ll) 1e6 + 10 , INF = (1ll << 61 ) - 1 ;static constexpr ll mod = 998244353 ; static constexpr double eps = 1e-8 ;const double PI = acos (-1.0 );ll lT, testcase; vector<ll> at_ls, ata_ls; ll siz_AT_repeat = 140 ; void init () { at_ls.resize (siz_AT_repeat); ata_ls.resize (siz_AT_repeat); for (int i = 1 ; i < siz_AT_repeat; ++i) { at_ls[i] = i; ata_ls[i] = i; } partial_sum (all (at_ls), at_ls.begin ()); partial_sum (all (ata_ls), ata_ls.begin ()); partial_sum (all (ata_ls), ata_ls.begin ()); #ifdef LOCAL debug1d (ata_ls); debug1d (at_ls); #endif } vector<ll> greedy_cal (const vector<ll> &A, ll x) { vector<ll> res; while (x != 0 ) { auto it = upper_bound (all (A), x); if (it == A.begin ()) { assert (0 ); } else { --it; res.push_back (it - A.begin ()); x -= *it; } } #ifdef LOCAL debug1d (res); #endif return res; } struct Ch_Num_CG { char ch; ll num_g; ll num_c; }; void opls_insert (vector<Ch_Num_CG> &ans, vector<ll> opls, ll offset, char cg) { sort (all (opls), greater ()); ll cnta = 0 ; for (int i = SZ (ans) - 1 ; i > offset; --i) { if (ans[i].ch == 'A' ) { cnta++; } while (!opls.empty () && opls.back () == cnta) { if (cg == 'G' ) { ans[i - offset].num_g++; } else { ans[i - offset].num_c++; } opls.pop_back (); } } } string gen_ans (const vector<Ch_Num_CG> &ans) { string res; for (auto [ch,num_g,num_c]: ans) { res += string (num_g, 'G' ); res += string (num_c, 'C' ); res += ch; } return res; } void Solve () { ll G, C; cin >> G >> C; vector<ll> g_opls = greedy_cal (ata_ls, G); vector<ll> c_opls = greedy_cal (at_ls, C); string s; vector<Ch_Num_CG> Ans; for (int i = 0 ; i < siz_AT_repeat + 5 ; ++i) { Ans.push_back ({'A' , 0 , 0 }); Ans.push_back ({'T' , 0 , 0 }); } opls_insert (Ans,g_opls,2 ,'G' ); opls_insert (Ans,c_opls,0 ,'C' ); cout << gen_ans (Ans) << "\n" ; } signed main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); #ifdef LOCAL cout.setf (ios::unitbuf); #endif init (); cin >> lT; for (testcase = 1 ; testcase <= lT; ++testcase) Solve (); return 0 ; }
心路历程(WA,TLE,MLE……)