- Problem A. 矩阵游戏
- Problem B. 整数生成器
- Problem C. 切牌
- Problem D. 生成魔咒
- Problem E. 网格染色
- Problem F. 排名预测
- Problem G. 货币系统
- Problem H. 松散子序列
- Problem I. 队伍取名
- Problem J. 解谜比赛
- Problem K. 打字机
- Problem L. 路线选择
比较难绷的一场比赛吧……大部分题目给我的感觉就是……代码量巨大,写之前比较费脑子但是写的时候无脑的
题解给的难度排名比较奇怪:
但是就我个人感觉而言,\(B\) 其实比大部分题目都要简单很多?
接下来是题解和代码,都写完了而且有些题真的很坐牢(
Problem A. 矩阵游戏
唯一一个写破防了的题目(
不是因为这道题具体有多难,而是这道题有点毒瘤。(把 long long 炸了的神仙题目啊!)
我们发现 \(m\) 的取值非常的小,于是显然想到这道题的复杂度最后可能与 \(2 ^ m\) 相关。
不难想到:
枚举列反转情况 \(t\) 与枚举每一个行原始状态状态 \(u\),可以得出异或后的行状态 \(s\),并将所有行状态为 \(s\) 的行存入到一个 vector \(v\) 里面。此时对于一个行状态为 \(s\) 的行 \(v_i\),我们可以很轻易的算出是否反转此行的贡献。记不反转为 \(w_1\),反转为 \(w_2\)
例如对于第 \(i\) 且行状态 \(s\),显然:
容易发现随着 \(i\) 的增长,\(w_1\) 和 \(w_2\) 的变化都是单调的,而且增速都是恒定的,也就是贡献随着行数增大都是单调变化的。
如此我们可以轻易发现:至多出现一个分界点 \(i\) 使得 \(v_i\) 之前的行不反转更优,之后的行反转更优(可能是反过来说,无所谓了)
我们可以通过二分获得这个分界点 \(i\),之后的做法就很显然了。
不过这题会炸 long long,所以在算答案的时候要用 __int128,还不能无脑用因为可能会 TLE……
题解和具体的代码实现可能有点出入,将就着看吧。
代码:
#include <iostream>
#include <vector>
#define int long longusing std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;
using big = __int128;int n, m, A, B;
std::vector<int> v[kMaxN];
std::vector<int> suf[kMaxN];
int popcnt[kMaxN];
int sucker[kMaxN]; // 不要管这个奇怪的数组名int toi(const std::string& str) {int ans = 0;for (int i = 0; i < str.size(); i++) {ans = (str[str.size() - i - 1] - '0') + (ans << 1);}return ans;
}int suck(int s) {int ans = 0;for (int i = 0; i < m; i++) {ans += ((s >> i) & 1) * B * (i + 1);}return ans;
}int pop(int x) {int cnt = 0;while (x) {cnt += x & 1;x >>= 1;}return cnt;
}// v1 = suck(i) + i * popcnt * A
// v2 = ((m + 1) * m / 2) * B + m * B * i - v1
// 顯然 v1 和 v2 都是單調的int v1(int i, int s) {return sucker[s] + i * popcnt[s] * A;
}int v2(int i, int s) {return ((m + 1) * m / 2) * B - sucker[s] + i * (m - popcnt[s]) * A;
}big v1sum(int i, int s, int s2) {if (i < 0) return 0;return (big)sucker[s] * (i + 1) + popcnt[s] * A * suf[s2][i];
}big v2sum(int i, int s, int s2) {if (i < 0) return 0;return (big)(((m + 1) * m / 2) * B - sucker[s]) * (i + 1) + suf[s2][i] * (m - popcnt[s]) * A;
}void upans(big& ans, int mid, int s, int s2) {if (mid < 0) return;ans = std::max(ans, v1sum(mid, s, s2) + v2sum(v[s2].size() - 1, s, s2) - v2sum(mid, s, s2));ans = std::max(ans, v2sum(mid, s, s2) + v1sum(v[s2].size() - 1, s, s2) - v1sum(mid, s, s2));
}void print(big s) {if (s < 10) {cout << (int)s;return;}print(s / 10);cout << (int)(s % 10);
}signed main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> m >> A >> B;for (int i = 1; i <= n; i++) {std::string str;cin >> str;v[toi(str)].push_back(i);int s = toi(str);popcnt[s] = pop(s);sucker[s] = suck(s);}for (int s = 0; s < (1 << m); s++) {popcnt[s] = pop(s);sucker[s] = suck(s);suf[s] = v[s];for (int i = 1; i < suf[s].size(); i++) {suf[s][i] += suf[s][i - 1];}}big ans = -1e18, sum, tmp;for (int s1 = 0; s1 < (1 << m); s1++) {sum = 0;for (int s2 = 0; s2 < (1 << m); s2++) {int s = s1 ^ s2;auto& v = ::v[s2];if (v.empty()) continue;// 抛弃大脑的写法,别学tmp = std::max(v1sum(v.size() - 1, s, s2), v2sum(v.size() - 1, s, s2));int l = 0, r = v.size() - 1, t = -1;while (l <= r) {int mid = (l + r) >> 1;if (v1(v[mid], s) - v2(v[mid], s) >= 0) {l = mid + 1;t = mid;} else {r = mid - 1;}}upans(tmp, t, s, s2);l = 0, r = v.size() - 1, t = -1;while (l <= r) {int mid = (l + r) >> 1;if (v1(v[mid], s) - v2(v[mid], s) >= 0) {r = mid - 1;t = mid;} else {l = mid + 1;}}upans(tmp, t, s, s2);l = 0, r = v.size() - 1, t = -1;while (l <= r) {int mid = (l + r) >> 1;if (v1(v[mid], s) - v2(v[mid], s) <= 0) {t = mid;l = mid + 1;} else {r = mid - 1;}}upans(tmp, t, s, s2);l = 0, r = v.size() - 1, t = -1;while (l <= r) {int mid = (l + r) >> 1;if (v1(v[mid], s) - v2(v[mid], s) <= 0) {r = mid - 1;t = mid;} else {l = mid + 1;}}upans(tmp, t, s, s2);sum += tmp;}if (s1 == 0) ans = sum;ans = std::max(ans, sum);}print(ans);// cout << ans << endl;return 0;
}
Problem B. 整数生成器
一道有意思的线性代数?线性基?题目。
我不认为这道题太难,但是考场上只有五个队写出来这道题还是挺疑惑的。
首先我们要明确一个事实:加法和异或其实没有那么大区别,他们构成的线性空间本质上甚至一样,二者的性质和定理可以相互使用,所以我们最后可以直接高斯消元求解。我不太清楚怎么描述这个数学现象,同构?还是什么。
首先维护一个线性基,然后将所有数字插入到这组线性基中。很显然题目给出的另外两个运算 \(and\) 和 \(or\) 是可能构造出线性无关的向量的,于是我们直接随机两个数然后 \(and\) \(or\) 直接插进去。由生日悖论可以得知这样做其实问题不大。
不过题解给出了更优秀的做法:把线性基中已有的数进行与或操作,正确性不会证(
由于线性基的大小不可能超过 \(32\),所以两种位运算的操作次数也不可能超过 \(32\)。题目给出的 \(70\) 上限其实是很难达到的。
最后我们对于这组线性基直接用它来高斯消元来获取题目中要求的 \(x\)。而且线性基的大小不可能超过 \(32\),不过线性基应该要额外保存好原始的数字,因为最后输出答案的时候要给出原始数。
代码:
#include <iostream>
#include <random>
#include <vector>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;class BASE {int val[32];int id[32];public:std::vector<int> v;bool insert(int x, int t) {for (int i = 31; i >= 0; i--) {if ((x >> i) & 1) {if (val[i]) {x ^= val[i];} else {val[i] = x;v.push_back(t);id[i] = t;return true; }}}return false;}
} sour;int matrix[34][34];
int a[kMaxN];
int n, x;std::vector<std::tuple<int, int, int>> ans;void calc() {int m = sour.v.size();for (int j = 0; j < m; j++) {for (int i = 31; i >= 0; i--) {matrix[i][j] = (a[sour.v[j]] >> i) & 1;}}for (int i = 31; i >= 0; i--) {matrix[i][32] = (x >> i) & 1;}for (int i = 0; i <= 31; i++) {if (matrix[i][i] != 1) {bool flag = false;for (int j = i + 1; j <= 31 && !flag; j++) {if (matrix[j][i] == 1) {for (int k = 0; k < 34; k++) std::swap(matrix[i][k], matrix[j][k]);flag = true;}}}if (matrix[i][i] != 1) continue;for (int j = 0; j <= 31; j++) {if (i == j) continue;if (matrix[j][i]) {for (int k = 0; k < 34; k++) {matrix[j][k] ^= matrix[i][k];}}}}std::vector<int> anslist;for (int i = 0; i <= 31; i++) {// 方程的秩不一致,说明无解if (matrix[i][i] == 0 && matrix[i][32] != 0) {return cout << -1 << endl, void();}if (matrix[i][32]) {anslist.push_back(sour.v[i]);}}if (anslist.size() == 1) {goto end;}ans.push_back(std::make_tuple(1, anslist[0], anslist[1]));// cout << 0 << ' ' << anslist[0] << ' ' << anslist[1] << endl;n++;a[n] = ::a[anslist[0]] ^ ::a[anslist[1]];for (int i = 2; i < anslist.size(); i++) {ans.push_back(std::make_tuple(1, n, anslist[i]));// cout << 0 << ' ' << n << ' ' << anslist[i] << endl;n++;a[n] = ::a[n - 1] ^ ::a[anslist[i]];}end:cout << ans.size() << endl;for (auto[a, b, c] : ans) {cout << a << ' ' << ::a[b] << ' ' << ::a[c] << endl;}
}std::random_device seed;
std::mt19937_64 e(seed());int rand(int l, int r) {std::uniform_int_distribution<int> rd(l, r);return rd(e);
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> x;int cnt = 0;for (int i = 1; i <= n; i++) {cin >> a[i];cnt += sour.insert(a[i], i);}if (x == 0) {cout << 1 << endl;cout << 1 << ' ' << a[1] << ' ' << a[1] << endl;return 0;}for (int i = 1; i <= 1e5 && cnt < 40; i++) {int x, y;do {x = rand(1, n), y = rand(1, n);} while (x == y);if (cnt < 40 && sour.insert(a[x] & a[y], n + 1)) {n++;a[n] = a[x] & a[y];ans.push_back(std::make_tuple(2, x, y));cnt++;}if (cnt < 40 && sour.insert(a[x] | a[y], n + 1)) {n++;a[n] = a[x] | a[y];ans.push_back(std::make_tuple(0, x, y));cnt++;}}// cout << "AFTER INSERT" << endl;// for (auto i : sour.v) {//   cout << i << ' ';// }// cout << endl;calc();return 0;
}
不过这份代码事实上可以被 hack,但是官方数据居然没有叉掉我……
Problem C. 切牌
算是把我的智商摁在地上锤的聪明题。好玩。
官方题解写的很好我就放这里了:
首先注意到答案一定不会超过 \(n\), 因为如果目标序列前 \(k\) 个数是属于某种切分方式中每一堆的第一个数,那么切分和排列方案是唯一的。
假设我们已经确定了前 \(k\) 个数是某一堆的第一个数,那么对于后面的 \(n − k\) 个数,每个数 \(x\) 一定与 \(x − 1\) 在同一堆,且 \(x − 1\) 在目标序列的位置 \(p_x−1\) 在 \(x\) 的位置 \(p_x\) 左边,我们把 \((p_x−1, p_x)\) 看成区间,是否合法的充要条件是 \(p_x−1 < p_x\) 且这些区间没有包含关系,证明如下:
必要性:如果有包含关系,假设 \(p_{x-1} < p_{y-1} < p_y < p_x\),那么因为 \(p_{x-1} < p_{y-1}\),\(x\) 所在堆要在 \(y\) 前面;又由于 \(p_y < p_x\),\(x\) 所在的堆要在 \(y\) 后面,产生矛盾。
充分性:因为知道前 \(k\) 个数,可以直接推出切分关系。由于没有区间包含,我们可以根据前 \(k\) 个数得到一个堆之间的顺序,直接按这个排列即可。
通过这个,我们可以推出一个结论:如果前 \(k\) 个数对应的切分是合法的,那么前 \(k + 1\) 个数的切分也是合法的,因为我们只是减少了区间数量,不会带来新的区间矛盾。于是,每次询问就变成了去找最大的不满足条件的 \(k\)。
记原序列为 \(a_i\),我们维护每个位置 \(x\) 是否满足 \(p_{a_x - 1} < x\) 以及对应的区间 \((p_{a_x - 1},\, x)\) 是否被右边一个位置的区间 \((p_{a_{x+1} - 1},\, x + 1)\) 包含。因为如果 \((p_{a_x - 1},\, x)\) 被某个区间包含,要么是 \((p_{a_{x+1} - 1},\, x + 1)\),要么 \((p_{a_{x+1} - 1},\, x + 1)\) 也被这个区间包含,即会在比 \(x\) 更右边的某个位置 \(y\) 产生矛盾。而我们的询问是找最右边会产生矛盾的位置,因此一定会找到该 \(y\)。
维护某个位置是否合法可以在 \(O(1)\) 时间内完成。每次交换会影响 \(O(1)\) 个位置。查询最右边矛盾的位置可以用线段树实现。总体复杂度为\(O(n \log n)\)
其实也可以用树状数组实现其实(
代码:
#include <iostream>
#include <algorithm>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;int n, q;
int a[kMaxN];
int p[kMaxN];
bool bad[kMaxN];class SegmentTree {struct node {int val;node* son[2];int l, r;} a[kMaxN], *root;int tot = 0;void pushup(node *p) {p->val = std::max(p->son[0]->val, p->son[1]->val);// p->val = p->son[0]->val | p->son[1]->val;}void build(node *p, int l, int r) {p->val = 0, p->l = l, p->r = r;if (l == r) return;int mid = (l + r) >> 1;p->son[0] = a + (tot++);p->son[1] = a + (tot++);build(p->son[0], l, mid);build(p->son[1], mid + 1, r);}void upd(node *p, int x, int v) {if (p->l > x || p->r < x) exit(-1);if (p->l == p->r) return p->val = v, void();if (p->son[0]->r >= x) upd(p->son[0], x, v);if (p->son[1]->l <= x) upd(p->son[1], x, v);pushup(p);}public:int query() {return root->val;}void build() {root = a + (tot++);build(root, 1, n);}void upd(int p, int v) {if (p < 1) return;if (p > n) return;// cout << "UPD " << p << ' ' << v << endl;upd(root, p, v);pushup(root);}
} tree;int checkbad(int i) {if (i <= 0) return 0;if (i > n) return 0;if (a[i] ==  1) return 1;if (p[a[i] - 1] > i) return 1;if (i < n) {if (p[a[i + 1] - 1] <= p[a[i] - 1]) return 1;}return 0;
}void run() {// bad 表示在i之前进行分割是错误的for (int i = 0; i <= n; i++) bad[i] = false;for (int i = 1; i <= n; i++) {if (a[i] == 1) {bad[i] = true;}if (p[a[i] - 1] > i) bad[i] = true;if (i < n) {if (p[a[i + 1] - 1] <= p[a[i] - 1]) bad[i] = true;}}bad[1] = true;int ans = 1;for (int i = n; i >= 1; i--) {if (bad[i]) break;ans++;}
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> q;for (int i = 1; i <= n; i++) {cin >> a[i];p[a[i]] = i;}tree.build();auto upd = [&](int x) {tree.upd(x, checkbad(x) * x);};for (int i = 1; i <= n; i++) {upd(i);}cout << n - tree.query() + 1 << endl;while (q--) {int x, y;cin >> x >> y;std::swap(a[x], a[y]);// 不是很想仔细去考虑会影响哪些位置了,直接这样抛弃大脑去写了p[a[x]] = x;p[a[y]] = y;upd(x);upd(y);upd(x - 1);upd(y - 1);upd(x + 1);upd(y + 1);upd(p[a[x] + 1]);upd(p[a[y] + 1]);upd(p[a[x] - 1]);upd(p[a[y] - 1]);upd(p[a[x] + 1] - 1);upd(p[a[y] + 1] - 1);upd(p[a[x] - 1] - 1);upd(p[a[y] - 1] - 1);upd(p[a[x] + 1] + 1);upd(p[a[y] + 1] + 1);upd(p[a[x] - 1] + 1);upd(p[a[y] - 1] + 1);cout << n - tree.query() + 1 << endl;}return 0;
}
Problem D. 生成魔咒
啊?这题真的需要讲吗?
代码:
#include <iostream>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';void solve() {long long n;cin >> n;long long t = 0;long long ans = 0;while (n) {ans += (1ll << t) * (n % 10);n /= 10;t++;}cout << ans << endl;
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);int t;cin >> t;while (t--) solve();return 0;
}
Problem E. 网格染色
考场上 0 人通过的细节贪心题。当然其实不是最难的那道就是喽……
但是似认为这道题难度其实低于 B。
我们的染色操作只能减少联通块数量。 而且任何可以连接两个以上联通块的情况一定可以被分割成若干次连接两个联通块的方法,所以我们接下来只需要考虑连接两个联通块的操作。
有效的染色的操作主要有两种:
* * 1 0 0 0 1 * *    * * 1 1 1 1 1 * *
* * * * * * * * * -> * * * * * * * * *  
数字相同的在同一列,显然两个数可以直接连起来
另外一种情况:不在同一列或者被其他数字挡掉
* * 1 0 0 * * *
* * * 0 0 1 * *
* * 1 0 0 * 1 * *
* * * 0 0 0 0 * *
* * * 0 0 * 1 * *
* * 1 0 0 0 0 * *
当然,我们需要注意一个很特别的情况:
* 2 0 1 1 1 *
* * 0 0 0 0 2
这种情况下填 1 是没有任何收益的。所以我们选择填数的时候,这个数一定要保证已经出现在两个联通块中,同时填完了之后刷新一下所属联通块就好。
复杂度显然 \(O(n)\),但是我太懒了用了 set 所以复杂度比较玄学。
代码:
#include <iostream>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;int n;
int a[3][kMaxN];
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
int dfntot = 0;
int dfn[3][kMaxN];int fa[kMaxN];int init(int x, int y) {return (x - 1) * n + 10 + y;
}// 维护所属联通快
void update(int x, int y, int t) {std::queue<std::pair<int, int>> q;auto record = [&](int tx, int ty) {if (tx < 1 || tx > 2 || ty < 1 || ty > n) return;if (a[tx][ty] != a[x][y]) return;if (dfn[tx][ty] == t) return;dfn[tx][ty] = t;q.push({tx, ty});};record(x, y);while (!q.empty()) {auto[x, y] = q.front();q.pop();for (int i = 0; i < 4; i++) {record(x + dx[i], y + dy[i]);}}
}void flush(std::vector<std::pair<int, int>>& v, int col, int mergeto = -1) {for (auto [x, y] : v) {a[x][y] = col;if (mergeto != -1) {dfn[x][y] = mergeto;}}
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n;for (int i = 1; i < kMaxN; i++) fa[i] = i;for (int i = 1; i <= 2; i++) {for (int j = 1; j <= n; j++) {cin >> a[i][j];}}for (int i = 1; i <= 2; i++) {int lastcol = -1;for (int j = 1; j <= n; j++) {if (lastcol && a[i][j] == lastcol && a[i][j - 1] == 0) {for (int k = j - 1; k >= 1 && a[i][k] == 0; k--) {a[i][k] = lastcol;}}if (a[i][j]) lastcol = a[i][j];}}for (int j = 1; j <= n; j++) {if (a[1][j] && dfn[1][j] == 0) {update(1, j, ++dfntot);}if (a[2][j] && dfn[2][j] == 0) {update(2, j, ++dfntot);}}int lastcol = 0;std::vector<std::pair<int, int>> zeroblock;std::set<int> inset;std::unordered_map<int, int> setcnt;auto record = [&](int x, int y) {if (x < 1 || x > 2 || y < 1 || y > n) return;if (a[x][y] == 0) return;if (inset.count(dfn[x][y])) return;inset.insert(dfn[x][y]);setcnt[a[x][y]]++;};auto check = [&](int x, int y) {if (x < 1 || x > 2 || y < 1 || y > n) return false;if (a[x][y] == 0) return false;return setcnt[a[x][y]] >= 2;};for (int j = 1; j <= n; j++) {if (zeroblock.size()) {// 如果不联通if ((a[1][j] + a[1][j - 1]) && (a[2][j] + a[2][j - 1])) {if (a[1][j] && a[1][j - 1] == 0) flush(zeroblock, a[1][j], dfn[1][j]);else if (a[2][j] && a[2][j - 1] == 0) flush(zeroblock, a[2][j], dfn[2][j]);else exit(-1);inset.clear();setcnt.clear();zeroblock.clear();}}if (a[1][j]) lastcol = a[1][j];if (a[2][j]) lastcol = a[2][j];if (a[1][j] == 0) zeroblock.push_back({1, j});if (a[2][j] == 0) zeroblock.push_back({2, j});for (int i = 1; i <= 2 && zeroblock.size(); i++) {if (a[i][j] != 0) continue;for (int k = 0; k < 4 && zeroblock.size(); k++) {record(i + dx[k], j + dy[k]);if (check(i + dx[k], j + dy[k])) {flush(zeroblock, a[i + dx[k]][j + dy[k]], dfn[i + dx[k]][j + dy[k]]);inset.clear();setcnt.clear();zeroblock.clear();}}}}if (zeroblock.size()) {if (lastcol == 0) lastcol = 1;flush(zeroblock, lastcol);zeroblock.clear();}for (int i = 1; i <= 2; i++) {for (int j = 1; j <= n; j++) {cout << a[i][j] << ' ';}cout << endl;}return 0;
}
Problem F. 排名预测
也是非常简单的一道题,算是让小白明白 XCPC 的罚时计算机制了。
不过要注意一点:如果一个题没有通过,那么这个题是不算罚时的。注意到这一点就可以去贪心了。
#include <iostream>
#include <algorithm>
#include <vector>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';void solve() {int n, a, b, s;cin >> n >> a >> b;std::vector<int> cnt(128, 0);std::vector<bool> pass(128, 0);std::vector<int> pro;cin >> s;int A = 0, B = 0;int tot = 0;for (int i = 1; i <= s; i++) {int t;char ch;std::string status;cin >> t >> ch >> status;if (pass[ch]) continue;if (status == "ac" || status == "pd") {pass[ch] = true;if (status == "pd") {pro.push_back(t + 20 * cnt[ch]);} else {A++;B += t + 20 * cnt[ch];}} else {cnt[ch]++;}}if (A > a || (A == a && B < b)) {return cout << 0 << endl, void();}std::sort(pro.begin(), pro.end());for (int i = 0; i < pro.size(); i++) {B += pro[i], A++;if (A > a || (A == a && B < b)) {return cout << i + 1 << endl, void();}}cout << -1 << endl;
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);int t;cin >> t;while (t--) solve();return 0;
}
Problem G. 货币系统
有趣的计数题,要是想到正确做法还是不难的。
先给 \(A_n\) 排序,记 \(cnt_i\) 为 \(f(x, n) = i\) 的数量,然后我们先预处理出小于 \(A_n\) 情况下所有数字的 \(f(x, n)\) 并且统计到 \(cnt_i\) 里面。
接下来我们需要想办法统计所有大于 \(A_n\) 的数字。当然这显然是荒诞的,TLE 会给你来上一巴掌。
我们可以发现对于任何大于 \(A_n\) 的数字,它一定要垫若干张 \(A_n\) 然后变成 \(1\) 到\(A_n\) 中的某一个数。不难想到对于任意 \(f(x, n) = 1\),\(x + A_n\) 可以为 \(cnt_2\) 产生贡献,\(x + 2 A_n\) 可以为 \(cnt_3\) 产生贡献……以此类推
于是对 \(cnt_i\) 取个前缀和一切都结束了(
代码:
#include <cassert>
#include <iostream>
#include <algorithm>
#define int long longusing std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 2e6 + 100;int n, m;
int ans[kMaxN];
int a[kMaxN];
int able[kMaxN], sum = 0;
int cnt[kMaxN];int query(int x) {if (x > a[n]) {return query(x % a[n]) + x / a[n];}if (ans[x] != -1) return ans[x];return ans[x] = (query(x % a[able[x]]) + x / a[able[x]]);
}void solve() {int x;cin >> x;if (x >= a[n]) {cout << cnt[a[n]] << ' ';} else {cout << cnt[x] << ' ';}
}signed main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> m;for (int i = 0; i < kMaxN; i++) ans[i] = -1;ans[0] = 0;for (int i = 1; i <= n; i++) cin >> a[i];std::sort(a + 1, a + 1 + n);int j = 1;for (int i = 1; i <= 1e6; i++) {while (j != n && a[j + 1] <= i) j++;able[i] = j;}for (int i = 1; i <= a[n]; i++) {cnt[query(i)]++;}for (int i = 1; i <= a[n]; i++) {cnt[i] += cnt[i - 1];}while (m--) {solve();}return 0;
}
Problem H. 松散子序列
非常经典的一道题啊,就是从本质不同子序列的转移上面动一下手脚就结束了。我感觉官方题解给的状态转移方程其实有问题,不过我的状态设计和他不太一样……
直接看代码吧,没什么好讲的。大部分设计其实可以参考本质不同子序列的写法。
代码:
#include <iostream>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 998244353;int inc(int a, int b) {return (a + b) >= MOD ? a + b - MOD : a + b;
}int dec(int a, int b) {return (a - b) < 0 ? a - b + MOD : a - b;
}int mul(int a, int b) {return (1ll * a * b) % MOD;
}int dp[kMaxN][128];void solve() {int n, k;std::string str;cin >> n >> k >> str;str = '#' + str;for (int i = 0; i <= n; i++) {for (int j = 'a'; j <= 'z'; j++) {dp[i][j] = 0;}}for (int i = 1; i <= n; i++) {int sum = 1;for (int j = 'a'; j <= 'z'; j++) {int l = (i - k - 1);if (l < 0) l = 0;sum = inc(sum, dp[l][j]);}for (int j = 'a'; j <= 'z'; j++) {if (j == str[i]) {dp[i][j] = inc(dp[i][j], sum);continue;}dp[i][j] = inc(dp[i][j], dp[i - 1][j]);}}int ans = 0;for (int j = 'a'; j <= 'z'; j++) {ans = inc(ans, dp[n][j]);}cout << ans << endl;
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);int t;cin >> t;while (t--) solve();return 0;
}
Problem I. 队伍取名
一个不是太难的冗斥。
通过枚举 \(id\) 来考虑被组成的人,接下来我们只需要去寻找两个人,使得这两个人和 \(id\) 名字中某两个数相同即可。
我们可以轻易统计出名字位置上 \((1, 3)\) 相同、 \((2, 3)\) 相同、 \((1, 2)\) 相同的人数,并且冗斥出来只有 \(1\)、\(2\)、\(3\) 相同的人数。于是就可以计算出答案了。
代码实现比较暴力,冗斥也是手动冗斥(
代码:
#include <iostream>
#include <cassert>
#include <unordered_map>
#define int long longusing std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;int n;
int name[kMaxN][4];
int tot = 0;
int cnt[4][kMaxN];
std::unordered_map<int, int> cnt1[kMaxN];
std::unordered_map<int, int> cnt2[kMaxN];
std::unordered_map<int, int> cnt3[kMaxN];int dp[kMaxN][8];int calc(int v) {return (v - 1) * v / 2;
}signed main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n;for (int i = 1; i <= n; i++) {for (int j = 1; j <= 3; j++) {cin >> name[i][j];cnt[j][name[i][j]]++;}// 不可能有人同時在 cnt1 和 cnt2 中有計數,除了他自己cnt1[name[i][1]][name[i][2]]++;cnt2[name[i][1]][name[i][3]]++;cnt3[name[i][2]][name[i][3]]++;}// for each i, find a (j, k) st the have two same val to iint ans = 0;for (int i = 1; i <= n; i++) {dp[i][1] = cnt[1][name[i][1]] - 1;dp[i][2] = cnt[2][name[i][2]] - 1;dp[i][4] = cnt[3][name[i][3]] - 1;dp[i][3] = cnt1[name[i][1]][name[i][2]] - 1;dp[i][5] = cnt2[name[i][1]][name[i][3]] - 1;dp[i][6] = cnt3[name[i][2]][name[i][3]] - 1;dp[i][1] -= dp[i][3] + dp[i][5];dp[i][2] -= dp[i][3] + dp[i][6];dp[i][4] -= dp[i][5] + dp[i][6];for (int k = 1; k <= 6; k++) {for (int j = k + 1; j <= 6; j++) {ans += dp[i][j] * dp[i][k];}}ans += calc(dp[i][3]) + calc(dp[i][5]) + calc(dp[i][6]);}cout << ans << endl;return 0;
}
Problem J. 解谜比赛
官方题解说这道题是拓扑排序,但是我并没有从题目中看到保证 DAG 的信息……我怎么感觉其实是 dijkstra 的变形?
对于强制刷新器有两种处理办法:
- 
直接设定其入队的时间 
- 
设置一个虚拟节点,然后将贡献调整为 \(\infty\) 
我们对于任何一个点,维护一个 heap (或者是优先队列,随便了)用来存放最早提供贡献的 \(k\) 个时间点,当 \(k \geq a_i\) 时就可以将 \(i\) 入队了。具体实现看代码。
代码:
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
#include <set>
#define int long longusing std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 2e6 + 10;std::vector<std::pair<int, int>> go[kMaxN];struct type {int t, id;bool operator < (const type& p) const {if (t == p.t) return id < p.id;return t < p.t;}
};bool used[kMaxN];
int n, m, k;
std::set<type> s;
int ubtime[kMaxN];
int a[kMaxN], power[kMaxN];
std::priority_queue<int> q[kMaxN];void record(int t, int id) {if (used[id]) return;if (t > ubtime[id]) return;s.erase({ubtime[id], id});ubtime[id] = t;s.insert({t, id});
}int vtot = 0;void dijkstra() {while (!s.empty()) {auto[t, id] = *s.begin();s.erase(s.begin());if (used[id]) continue;used[id] = true;for (auto[v, w] : go[id]) {if (a[v] == 0) {record(ubtime[id] + w, v);continue;}if (q[v].size() >= a[v]) q[v].pop();q[v].push(ubtime[id] + w);if (q[v].size() >= a[v]) {record(q[v].top(), v);}}}
}signed main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> m >> k;memset(ubtime, 0x3f, sizeof(ubtime));for (int i = 1; i <= n; i++) {cin >> a[i];if (a[i] == 0) {record(0, i);}}vtot = n + 10;for (int i = 1; i <= k; i++) {int t, s, id;cin >> t >> s;for (int i = 1; i <= s; i++) {cin >> id;record(t, id);}}for (int i = 1; i <= m; i++) {int u, v, w;cin >> u >> v >> w;go[u].push_back({v, w});}dijkstra();for (int i = 1; i <= n; i++) {if (used[i] == false) ubtime[i] = -1;cout << ubtime[i] << ' ';}return 0;
}
Problem K. 打字机
其实不是太难的一个字符串题目……只是我觉得我像个傻子。
这道题其实是有 \(O(n)\) 的妙妙做法的,大概思路就是用 \(Manacher\) 处理掉回文的问题,然后跑一遍 \(z函数\)。我没有仔细想这个方向所以用 \(hash\) 跑了一个 \(O(n \log n)\) 的做法,最后统计答案的时候写的也不是很优雅。
我们将题目进一步抽象:
找到一个字符串形如 \(T = (aLb), |L| \geq 0\),使得 \((aLbL^R)^{\infty}\) 的一个前缀是 \(S\)。
当然有特殊情况就是 \(|T| = 1\) 的情况,这里要特殊处理一下。我当时就是因为没有特殊处理卡死了好久 QAQ。
容易发现一个正确的 \(T\) 是可以为若干个连续的前缀 \(S'\) 提供贡献的,这对应一个区间。
容易发现最小循环周期不是 \(T\) 而是 \(aLbL^R\),我们可以通过倍增 + 二分来得到区间右端点。
当然这里还需要特殊考虑一种情况:\(S\) 的长度不允许你跑完一个完整的周期 \(aLbL^R\),这里也要特殊处理一下。
代码,当时调试的时候以为自己哈希冲突了所以写了个双哈希:
#include <cassert>
#include <set>
#include <vector>
#include <algorithm>
#include <iostream>
#define int long longusing std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;int n;
std::string str;template<int MOD>
class hasher { int hash_[kMaxN];int revhash_[kMaxN];int inc(int x, int y) { return (x + y) >= MOD ? x + y - MOD : x + y; }int dec(int x, int y) { return (x - y) < 0 ? x - y + MOD : x - y; }int mul(int x, int y) { return (1ll * x * y) % MOD; }int powch[kMaxN];int invch[kMaxN];int pow(int x, int p) {int ans = 1;while (p) {if (p & 1) ans = mul(ans, x);p >>= 1, x = mul(x, x);}return ans;}public:int kk[kMaxN];int hash(int l, int r) {assert(l <= r);return dec(hash_[r], mul(hash_[l - 1], powch[r - l + 1]));}int revhash(int l, int r) { return dec(revhash_[l], mul(revhash_[r + 1], powch[r - l + 1])); }void init(const std::string& str) {hash_[0] = revhash_[n + 1] = 0;for (int i = 1; i <= n; i++) {hash_[i] = inc(mul(hash_[i - 1], powch[1]), str[i]);}for (int i = n; i >= 1; i--) {revhash_[i] = inc(mul(revhash_[i + 1], powch[1]), str[i]);}}hasher() {powch[0] = invch[0] = 1;powch[1] = 128, invch[1] = pow(128, MOD - 2);for (int i = 2; i < kMaxN; i++) {powch[i] = mul(powch[i - 1], powch[1]);invch[i] = mul(invch[i - 1], invch[1]);}}void upkk(int mid) {kk[0] = hash(1, mid);for (int i = 1; i <= 20; i++) {if ((1ll << i) * mid > kMaxN) break;kk[i] = inc(kk[i - 1], mul(kk[i - 1], powch[(1 << (i - 1)) * mid]));}}
};hasher<998244353> hash1;
hasher<(int)(1e9 + 7)> hash2;int kk[30];
int kk2[30];
int cnt[kMaxN];
std::vector<int> begin[kMaxN];
std::vector<int> end[kMaxN];void run(int mid) {if (mid == n) return;if (mid % 2) return;if (mid != 2) {int mid2 = mid >> 1;if (hash1.hash(2, mid2) != hash1.revhash(mid2 + 2, mid)) return;if (hash2.hash(2, mid2) != hash2.revhash(mid2 + 2, mid)) return;}hash1.upkk(mid);hash2.upkk(mid);int lastp = mid;for (int i = 20; i >= 0; i--) {long long nextp = (lastp + 1ll * mid * (1 << i));if (nextp > n) continue;if (hash1.kk[i] == hash1.hash(lastp + 1, nextp) &&hash2.kk[i] == hash2.hash(lastp + 1, nextp)) {lastp = nextp;}}int l = 1, r = mid, able = 0;while (l <= r) {int mid = (l + r) >> 1;if (lastp + mid <= n &&hash1.hash(lastp + 1, lastp + mid) == hash1.hash(1, mid) &&hash2.hash(lastp + 1, lastp + mid) == hash2.hash(1, mid)) {l = mid + 1;able = mid;} else {r = mid - 1;}}lastp += able;cnt[mid / 2 + 1]++;end[lastp].push_back(mid / 2 + 1);
}void runtwo(int t) {int Llen = t - 2;int l = 1, r = Llen, ans = 0;while (l <= r) {int mid = (l + r) >> 1;if (t + mid <= n && hash1.revhash(t - mid, t - 1) == hash1.hash(t + 1, t + mid) &&hash2.revhash(t - mid, t - 1) == hash2.hash(t + 1, t + mid)) {l = mid + 1;ans = mid;} else {r = mid - 1;}}if (ans != 0) {cnt[t]++;end[t + ans].push_back(t);}
}bool bfcheck(const std::string& str, const std::string& sub) {int v = 1, p = 0;for (int i = 0; i < str.size(); i++) {if (str[i] != sub[p]) return false;if (p + v >= sub.size() || p + v < 0) v *= -1;p += v;if (sub.size() == 1) p = 0;}return true;
}std::set<int> s;
void solve() {s.clear();cin >> str;n = str.size(); str = '#' + str + '#';bool flag = true;for (int i = 2; i <= n && flag; i++) {flag &= str[i] == str[i - 1];}if (flag) {int ans = 0;for (int i = 1; i <= n; i++) {ans ^= 1 * i;}return cout << ans << endl, void();}hash1.init(str);hash2.init(str);for (int i = 2; i <= n; i++) {run(i);}for (int i = 2; i < n; i++) {runtwo(i);}cnt[1]++;for (int i = 2; i <= n; i++) {if (str[i] != str[i - 1]) {end[i - 1].push_back(1);break;}}int ans = 0;for (int i = 1; i <= n; i++) {if (cnt[i]) s.insert(i);// if (caninsert[i]) s.insert(i);int tmp;if (s.empty()) {tmp = i;ans ^= i * i;} else {tmp = *s.begin();ans ^= i * (*s.begin());}for (auto v : end[i]) {cnt[v]--;if (cnt[v] == 0) s.erase(v);}end[i].clear();}for (int i = 1; i <= n; i++) cnt[i] = 0;cout << ans << endl;
}signed main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);int t;cin >> t;while (t--) solve();return 0;
}
Problem L. 路线选择
嗯……虽然作为一个插头 DP 看上去真的很吓人好不好……但是写完了感觉这个插头 DP 其实还是挺板的一道题。
考虑状态设计:由于我们是从上到下开始遍历,所以同时只会存储横向至多 \(4\) 个点的状态。对于每一个点一共有两个状态需要处理:已经经过的路径度奇偶性和所属联通块。可以发现这个状态其实非常小,于是直接压缩进一个数内,其数量大概也就是 \(2 * (m + 1)^m\),事实上非常小。
至于状态转移,一共有六种情况,这里可以看代码里的注释。
代码:
// 非常hard的一道题,考场上能写出来的那队真的是神仙,甚至还是一遍过
#include <iostream>
#include <ctime>
#include <cstring>
#include <algorithm>
#include <vector>using std::cin;
using std::cout;
using std::cerr;const char endl = '\n';
const int kMaxN = 1e6 + 100;template <class type>
void upmin(type& a, const type& b) { a = std::min(a, b); }template <class type>
void upmax(type& a, const type& b) { a = std::max(a, b); }struct path {int v;std::vector<double> pos;// 0 不走// 1 走一次// 2 来回// 3 左边走到最右边但不到点/上面走到最下面但不到点// 4 右边走到最左边但不到点/下面走到最上面但不到点// 5 两个点同时走但是在中间一个地方回头double val[6];void init() {double c = 1.0 / v;val[1] = c, val[2] = c * 2;if (pos.empty()) return;std::sort(pos.begin(), pos.end());val[0] = 1e18;val[3] = 2 * pos.back() * c;val[4] = 2 * (1 - pos.front()) * c;pos.push_back(1);val[5] = 1e18;for (int i = 0; i < pos.size(); i++) {upmin(val[5], (i ? pos[i - 1] * 2 * c : 0) + ((1 - pos[i]) * 2 * c));}}
};path pathR[70][10], pathC[70][10];// 状态构造:每个端点出度的奇偶性+所属于的联通块,然后把所有点的信息压缩到一个数字内。不难证明数量不超过 10^m (上限)
double dp[2][10010];
bool has[1145][1145];int n, m, k;bool isInt(double x) {return x == (int)x;
}std::pair<int, int> nextState_[10010][10][7][7];
bool vis[10010][10][7][7];int total = 1;
int cnt[6], bel[6], deg[6];void clear() {memset(cnt, 0, sizeof(cnt));memset(bel, 0, sizeof(bel));memset(deg, 0, sizeof(deg));
}// 考虑从(u, p)转移到的下一个状态,同时走法是(i, j),i考虑列走法,j考虑行走法
// 两个值代表不经过和经过
const std::pair<int, int>& nextState(int u, int p, int i, int j) {if (vis[u][p][i][j]) return nextState_[u][p][i][j];auto& state = nextState_[u][p][i][j];vis[u][p][i][j] = true;clear();// cnt = std::vector<int>(5, 0);// bel = std::vector<int>(m + 1, 0);// deg = std::vector<int>(m + 1, 0);int now = u;for (int t = m - 1; t >= 0; t--) {int tmp = now % (2 * (m + 1));now /= 2 * (m + 1);deg[t] = tmp / (m + 1);bel[t] = tmp % (m + 1);cnt[bel[t]]++;}if ((deg[p] ^ (i == 1)) ||(bel[p] == 0 && i != 0 && i != 4) ||(bel[p] && cnt[bel[p]] == 1 && (i != 1 && i != 2)) ||(p && bel[p - 1] == 0 && j != 0 && j != 4) ||(j && p == 0)) {return state;}int BEL = bel[p];deg[p] = bel[p] = 0;if ((i == 0 || i == 3) && (j == 0 || j == 3)) {int nxt = 0;for (int t = 0; t < m; t++) {nxt *= 2 * (m + 1);nxt += deg[t] * (m + 1) + bel[t];}state.first = nxt;} else {if (i == 1 || i == 2) {deg[p] ^= i == 1;bel[p] = BEL;}if (j == 1 || j == 2) {deg[p] ^= j == 1;deg[p - 1] ^= j == 1;int nxt = bel[p - 1];if (bel[p]) {for (int t = 0; t < m; t++) {if (bel[t] == BEL) bel[t] = nxt;}} else {bel[p] = nxt;}}}for (auto& i : cnt) i = 0;for (int i = 0; i < m; i++) {cnt[bel[i]]++;}if (bel[p] == 0) {for (int i = 1; i <= m; i++) {if (cnt[i] == 0) {bel[p] = i;break;}}}int nxt = 0;for (int i = 0; i < m; i++) {nxt *= 2 * (m + 1);nxt += deg[i] * (m + 1) + bel[i];}state.second = nxt;return state;
}int main() {std::ios::sync_with_stdio(false);cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);cin >> n >> m >> k;for (int i = 0; i < n; i++) {for (int j = 0; j < m - 1; j++) {cin >> pathR[i][j].v;}}for (int i = 0; i < n - 1; i++) {for (int j = 0; j < m; j++) {cin >> pathC[i][j].v;}}for (int i = 1; i <= k; i++) {double x, y;cin >> x >> y;int ix = (int)x, iy = (int)y;if (!isInt(x)) {pathC[ix][iy].pos.push_back(x - ix);} else if (!isInt(y)) {pathR[ix][iy].pos.push_back(y - iy);} else {has[ix][iy] = true;}}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {pathC[i][j].init(), pathR[i][j].init();}}for (int i = 1; i <= m; i++) {total *= (m + 1) * 2;}for (int i = 0; i < 10000; i++) {dp[0][i] = 1e18;}int begin = m + 1 + 1;for (int i = 1; i < m; i++) begin *= 2 * (m + 1);dp[0][begin] = 0;int now = 0, lst = 1;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (i == 0 && j == 0) continue;now ^= 1, lst ^= 1;for (int t = 0; t < total; t++) {dp[now][t] = 1e18;}for (int u = 0; u < total; u++) {if (dp[lst][u] >= 1e18) continue;for (int x = 0; x <= 5; x++) {if (i == 0 && x != 0) continue;for (int y = 0; y <= 5; y++) {if (j == 0 && y != 0) continue;const auto&[s1, s2] =  nextState(u, j, x, y);if (has[i][j] == false && s1 != -1) {upmin(dp[now][s1], dp[lst][u] +(j == 0 ? 0 : pathR[i][j - 1].val[y]) +(i == 0 ? 0 : pathC[i - 1][j].val[x]));}if (s2 != -1) {upmin(dp[now][s2], dp[lst][u] +(j == 0 ? 0 : pathR[i][j - 1].val[y]) +(i == 0 ? 0 : pathC[i - 1][j].val[x]));}}}}}}double ans = 1e18;for (int u = 0; u < total; u++) {clear();{int now = u;for (int t = m - 1; t >= 0; t--) {int tmp = now % (2 * (m + 1));now /= 2 * (m + 1);deg[t] = tmp / (m + 1);bel[t] = tmp % (m + 1);cnt[bel[t]]++;}}bool flag = true;flag &= bel[m - 1] != 0;flag &= deg[m - 1] == 1;flag &= cnt[0] + cnt[bel[m - 1]] == m;for (int i = 0; i + 1 < m; i++) flag &= deg[i] == 0;if (flag) upmin(ans, dp[now][u]);}printf("%.9lf", ans);// cout << ans << endl;return 0;
}