第一题跟第二题很简单,代码量也不大(好实现),因此本文主要讲解第三题和第四题。
第三题
简略讲一下思路吧。
其实就是一个小贪心+模拟。
首先题目要求我们用n个小木棍去拼凑出尽可能小的数(不含前导0)。
这个时候就要想,我们在比较两个数之间的大小关系时,先看什么东西。
思考ing......
不含前导0!!!
对于两个数之间大小关系的比较,我们通常先看数的位数,位数多的那个数就一定比较大,因此我们如果要拼凑出尽可能小的数,首先需要最小化数的位数。
最小化数的位数——那么对于每个位,我们需要用掉尽可能多的小木棍(贪心)
显然,数字 '8'是消耗最多木棍的数,因此,我们先把所有木棍都去拼数字‘8’
这个时候就会产生一个新的问题:小木棍有剩余。(剩余木棍的数量 < 7)
对于这些剩余木棍,我们需要将前面的‘8’拆分一下,然后再去拼凑出数字。
对于拼凑数字的情况就非常暴力了。自己打一下表即可。
第四题
动态规划+优化
首先注意到题目给定两个条件:
1、每个人选择子数组的长度为[2, k],以及接龙规则。
2、同一个人不能连续两个回合出现,假设现在是第i个回合,上一回合(i - 1个回合)用的A, 那么当前(第 i 个)回合就不能用A了,但是在下一个回合(第 i + 1个回合)就能再次使用A。
然后是求在进行 Ri 回合之后得到的字符串能否以 Ci 结尾。
显然是DP了对吧
那么DP属性怎么选呢?
注意到题目要我们求的是 Ri 回合之后得到的字符串能否以 Ci 结尾。因此DP数组的属性为bool值
选好了DP数组的属性之后,我们要考虑DP的维度,用多少维合适呢?一维?二维?三维?
首先需要记录一下第几轮,比如第 i 轮需要从第 i - 1轮转移,这需要一维
这个时候就需要结合一开始提到的两个条件了,首先需要满足接龙条件——上回合的末尾字符与当前回合的首字符相等——保存上一轮的最后一个字符, 这需要一维
最后,需要维护上一回合进行操作的是哪个人(连续两个回合的人不能相同), 这同样需要一维
综上,初步分析DP数组需要三维,规模是R * n * A, R为轮数,n为人数,A为最大的数字
显然是不行的,因此进行本题的第二步——DP优化。
首先R的范围是100, n的范围是100000,A的范围是200000
因此,必须优化掉 n 或者 A。
这个时候我们不确定优化掉哪个,因此我们可以都试一试。
首先尝试优化掉A,那么显然是不可行的,因为我们的转移必须满足接龙规则,即我们在转移的时候需要枚举上一个数字是什么。
其次是优化n,现在的DP状态为f[r][v],表示进行r轮操作,以v为结尾是否是可行的。
那么这个时候状态当中就缺少了上一个回合是哪个人这个信息,现在考虑怎么去维护需要上一个回合是哪个人。注意到,对于一个状态f[r - 1][v],假设f[r - 1][v]可以通过A和B进行转移,即在第r - 1回合选择选手A和B都是合法的。那么对于当前枚举的这个人C来说,是不是只要找一个f[r - 1][c]除了通过自己进行转移的合法状态进行转移即可。观察一下可以发现,对于每个状态f[r - 1][v],我们只需要维护两个可以转移的人即可, 即f[r - 1][v] = {A, B}。那么只要在集合{A, B}中存在一个元素与当前枚举的C选手不同即可进行合法转移(连续两个回合的选手不同)。规模是R * A!!!
至此,这道题的思路就已经结束,实现上可能与上述有些许不同,见代码注释
#include<bits/stdc++.h>
using namespace std;
void solve(){
int n, k, q;
int mxr = -1, mxc = -1;
cin >> n >> k >> q;
vector<int>L(n + 1);
vector<vector<int>>S(n + 1);
for(int i = 1; i <= n; i++){
cin >> L[i];
S[i].resize(L[i]);
for(int j = 0; j < L[i]; j++){
cin >> S[i][j];
mxc = max(mxc, S[i][j]);
}
}
//cin >> q;
vector<pair<int,int>>ask(q);
for(int i = 0; i < q; i++){
cin >> ask[i].first >> ask[i].second;
mxr = max(mxr, ask[i].first);
}
//f[i][j] 表示i轮,以j结尾的情况是否能够被构造出来
vector<vector<int>>f(mxr + 1, vector<int>(mxc + 1, -1));
//初始状态,必须从1开始,那么第0次操作(实际上是不存在的)的结尾就是 1
f[0][1] = 0;
//1、如何判断上一轮是哪个人进行操作,通过f[i][j]存储的值进行判断
//-1 表示不能完成任务, num && num != 0表示上一次操作的人是num, num = 0 表示上一次操作的人可以是{p1, p2, p3, ...}
//2、如何判断每个人的序列长度
//所谓的单调队列优化!!!
//比如...
//假设j > i, 能够从j转移就不必从i转移
//枚举轮数
for(int i = 1; i <= mxr; i++){
//枚举每个人
for(int j = 1; j <= n; j++){
//-1表示上一个不能转移
int lst = -1;
for(int p = 0; p < L[j]; p++){
int v = S[j][p];
//最近能转移的点是lst,如果这个距离大于k,那么表示不能转移
if(p - lst >= k){
//第j个人不能选择子数组[l, p],因为长度大于k
lst = -1;
}
if(lst != -1){
//更进行本轮循环的转移,操作之后自然是从 i - 1 --> i
if(f[i][v] == -1){
f[i][v] = j;
}
//多于两个状态能进行转移
else if(f[i][v] != j)f[i][v] = 0;
}
//更新最右能转移的端点
//注意,这里是更新的左端点,因此i - 1轮的最后一个值应该是v
if(f[i - 1][v] != -1 && f[i - 1][v] != j)lst = p;
}
}
}
for(auto &[r, c] : ask){
if(c > mxc)
cout << 0 << endl;
else if(f[r][c] != -1)
cout << 1 << endl;
else
cout << 0 << endl;
}
return;
}
int main(){
int t;
cin >> t;
while(t--)solve();
return 0;
}
为了对萌新友好,在这里解释一下类似单调队列的转移
因为在上面的代码中内层的两重循环相当于是在枚举右端点,那么这个时候我们就需要在右端点选择S[j][p]的前提下去看有没有合法的左端点(距离当前下标p的距离不超过k都可以转移),那么我们可以在尝试枚举左端点O(k),去枚举哪些距离下标p不超过k的左端点。
for(int i = 1; i <= mxr; i++){
//枚举每个人
for(int j = 1; j <= n; j++){
//-1表示上一个不能转移
int lst = -1;
for(int p = 0; p < L[j]; p++){
for(int left = p - 1; p - left < k; left--){
//代码逻辑
}
}
}
}
注意!!!这会超时,因为k是200000,这个时候我们只需要维护最大的合法转移下标lst,在转移的时候通过lst进行转移即可。
本文到此结束。