从这里开始
因为比赛的时候在路上,所以又成功错过下分和被神仙 jerome_wei 吊起来打(按在地上摩擦)的好机会。
Problem A Pay to Win
把这个过程倒过来。不难发现到下一次除之前,要么是加到 $lfloor n/d floor d$ 要么是 $lceil n/d ceil d$,或者直接减到 0.
直接用 map 记忆化的复杂度为 $O(T log^3 N log log N)$。具体的来说,每个状态只可能是 $lfloor frac{n}{2^a3^b5^c} floor$ 或者 $lceilfrac{n}{2^a3^b5^c} ceil$,对于 $ leftlceilfrac{lfloor frac{n}{u} floor }{v} ight ceil $ 的情况有 $left lfloorfrac{n}{uv} ight floor = leftlfloorfrac{lfloor frac{n}{u} floor }{v} ight floor leqslant leftlceilfrac{lfloor frac{n}{u} floor }{v} ight ceil leqslant leftlceilfrac{lceilfrac{n}{u} ceil}{v} ight ceil = leftlceilfrac{n}{uv} ight ceil$
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pli pair<ll, int>
template <typename T>
bool vmin(T& a, T b) {
return (a > b) ? (a = b, true) : false;
}
const ll llf = 1e18;
ll n;
int T, A, B, C, D;
map<ll, ll> F;
vector<pair<int, int>> tr;
ll dfs(ll n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return D;
} else if (F.count(n)) {
return F[n];
}
ll ret = llf;
if (n <= ret / D) {
ret = n * D;
}
for (auto t : tr) {
int d = t.first;
int c = t.second;
ll nn = n / d * d;
vmin(ret, dfs(nn / d) + (n - nn) * D + c);
nn = (n + d - 1) / d * d;
vmin(ret, dfs(nn / d) + (nn - n) * D + c);
}
return F[n] = ret;
}
void solve() {
cin >> n >> A >> B >> C >> D;
F.clear();
tr = vector<pair<int, int>> {make_pair(2, A), make_pair(3, B), make_pair(5, C)};
cout << dfs(n) << '
';
}
int main() {
cin >> T;
while (T--) {
solve();
}
return 0;
}
Problem B Joker
容易发现初始最短路之和为 $O(n^3)$。一个人离开后暴力更新会产生改变的位置,时间复杂度不会超过初始最短路之和。
Code
#include <bits/stdc++.h>
using namespace std;
typedef bool boolean;
template <typename T>
T smin(T x) {
return x;
}
template <typename T, typename ...K>
T smin(T a, const K &...args) {
return min(a, smin(args...));
}
const int N = 505;
const int mov[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int n;
int ans = 0;
int f[N][N];
int vis[N][N];
bool occupy[N][N];
queue<int> qx0, qy0, qx1, qy1;
void update(int x, int y) {
static int dfc = 0;
++dfc;
qx0.push(x);
qy0.push(y);
while (!qx0.empty() || !qx1.empty()) {
int ex = -1, ey = -1;
if (!qx0.empty()) {
ex = qx0.front();
ey = qy0.front();
qx0.pop();
qy0.pop();
} else {
ex = qx1.front();
ey = qy1.front();
qx1.pop();
qy1.pop();
}
if (vis[ex][ey] == dfc) {
continue;
}
vis[ex][ey] = dfc;
int w = f[ex][ey] + occupy[ex][ey];
for (int i = 0; i < 4; i++) {
int nx = ex + mov[i][0];
int ny = ey + mov[i][1];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= n && w < f[nx][ny]) {
f[nx][ny] = w;
if (!occupy[ex][ey]) {
qx0.push(nx);
qy0.push(ny);
} else {
qx1.push(nx);
qy1.push(ny);
}
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = smin(i - 1, j - 1, n - i, n - j);
occupy[i][j] = true;
}
}
for (int i = 1, _ = n * n, t, x, y; i <= _; i++) {
scanf("%d", &t);
x = (t - 1) / n + 1;
y = t - (x - 1) * n;
ans += f[x][y];
occupy[x][y] = false;
update(x, y);
}
printf("%d
", ans);
return 0;
}
Problem C Strange Dance
建一个 Trie 树,维护位置 $i$ 上的是谁。
对于 S 操作就是一个全局交换 2,3 子树。
对于 R 操作做一次循环位移,然后暴力递归有进位的子树。
Code
#include <bits/stdc++.h>
using namespace std;
int ans[540000];
typedef class TrieNode {
public:
TrieNode *ch[3];
bool swp12;
int id;
void swap12() {
swp12 ^= 1;
swap(ch[1], ch[2]);
}
bool leaf() {
return !ch[0];
}
void push_down() {
if (swp12) {
ch[0]->swap12();
ch[1]->swap12();
ch[2]->swap12();
swp12 = false;
}
}
void get_ans(int base, int v) {
if (leaf()) {
ans[id] = v;
return;
}
push_down();
ch[0]->get_ans(base * 3, v);
ch[1]->get_ans(base * 3, v += base);
ch[2]->get_ans(base * 3, v += base);
}
} TrieNode;
TrieNode pool[1000000];
TrieNode *_top = pool;
TrieNode *newnode() {
return _top++;
}
typedef class Trie {
public:
TrieNode* rt;
void build(TrieNode*& p, int r, int base, int v) {
p = newnode();
if (r == 0) {
p->id = v;
return;
}
build(p->ch[0], r - 1, base * 3, v);
build(p->ch[1], r - 1, base * 3, v += base);
build(p->ch[2], r - 1, base * 3, v += base);
}
void build(int n) {
build(rt, n, 1, 0);
}
void swap12() {
rt->swap12();
}
void update(TrieNode*& p) {
if (p->leaf()) {
return;
}
p->push_down();
swap(p->ch[0], p->ch[1]);
swap(p->ch[0], p->ch[2]);
update(p->ch[0]);
}
void update() {
update(rt);
}
void get_ans() {
rt->get_ans(1, 0);
}
} Trie;
int n;
char T[200005];
Trie tr;
int main() {
scanf("%d", &n);
scanf("%s", T + 1);
int m = strlen(T + 1);
tr.build(n);
for (int i = 1; i <= m; i++) {
char c = T[i];
if (c == 'S') {
tr.swap12();
} else {
tr.update();
}
}
tr.get_ans();
int all = 1;
for (int i = 1; i <= n; i++) {
all *= 3;
}
for (int i = 0; i < all; i++) {
printf("%d ", ans[i]);
}
return 0;
}
Problem D Guess the Password
考虑询问 62 次长度为 128 的全 a 串,全 b 串.....然后可以知道每种字符有多少个。
注意到如果有一个长度为 $l$ 的串是原串的子序列,那么如果在这个串中插入一个字符 $c$ 使得询问的结果减少 1,这意味着插入后仍然是原串的子序列。
考虑如果我们已经知道两个字符集不相交的串 $s, t$ 分别是原串的子序列,我们怎么把它们合并。考虑在 $s$ 的每个位置依次插入 $t$ 的下一个未确定字符,然后判断它是否是原串的子序列。
然后做一个简单归并就好了。
Code
#include <bits/stdc++.h>
using namespace std;
int query(string s) {
cout << "? " << s << '
';
cout.flush();
int dis;
cin >> dis;
return dis;
}
int L;
string charset;
vector<string> strs;
string merge(int l, int r) {
if (l == r) {
return strs[l];
}
int mid = (l + r) >> 1;
string sl = merge(l, mid);
string sr = merge(mid + 1, r);
string cur = "";
int should = L - sl.length();
int pr = 0, _pr = (signed) sr.size();
for (int i = 0; i < (signed) sl.size(); i++) {
while (pr < _pr && query(cur + sr[pr] + sl.substr(i)) == should - 1) {
cur += sr[pr++];
should--;
}
cur += sl[i];
}
while (pr < _pr) {
cur += sr[pr++];
}
return cur;
}
int main() {
for (int i = 0; i < 26; i++) {
charset += (char) ('a' + i);
}
for (int i = 0; i < 26; i++) {
charset += (char) ('A' + i);
}
for (int i = 0; i < 10; i++) {
charset += (char) ('0' + i);
}
L = 0;
for (int i = 0; i < 62; i++) {
char c = charset[i];
string s;
int cnt;
s.assign(128, c);
L += (cnt = 128 - query(s));
strs.push_back(s.substr(0, cnt));
}
string ans = merge(0, (signed) strs.size() - 1);
cout << "! " << ans << '
';
cout.flush();
return 0;
}
Problem E Random Pawn
首先假设第一个和最后一个都是 $A$ 中最大的,如果不是这样的话可以通过旋转,然后再在后面增加一个来实现。
因为到 $A$ 最大的地方一定会停止,因此现在问题变成了链上。
设 $E_i$ 表示从 $i$ 出发的最大期望收益,显然有 $E_i = max{frac{E_{i - 1} + E_{i + 1}}{2} - B_i, A_i}$。
考虑有 $B_i$ 非常地难处理,考虑把它搞掉。
设 $F_i = E_i - C_i$,那么有 $F_i = max{frac{E_{i - 1} - C_{i - 1} + E_{i + 1} - C_{i + 1}}{2}+ frac{C_{i - 1} + C_{i + 1}}{2} - C_i - B_i, A_i - C_i}$ 。
因此 $C_i$ 满足 $ frac{C_{i - 1} + C_{i + 1}}{2} - C_i - B_i = 0$。钦定 $C_0 = C_1 = 0$,然后可以简单构造出来。
注意到 $C_{i + 1} - C_i - 2B_i = C_i - C_{i - 1}$ 因此 $C_i$ 大概是 $2B_i$ 做二次前缀和,因此范围大概是 $10^{12}$ 左右。
考虑最终的序列中是硬点若干位置 $p$,使得 $F_p = A_p$。接下来假定 $A_i$ 都已经减去了 $C_i$。
考虑知道一段最左端为 $l$,最右端为 $r$ 时怎么计算中间的贡献。不难发现中间的 $F$ 满足 $F_{i} - F_{i - 1} = F_{i + 1} - F_i$,因此有 $F_i = frac{(i - l) A_r + (r - i) A_l}{r - l}$。
然后求一个和有 $sum_{l <i < r} F_i = frac{1}{2}(A_l + A_r) (r - l - 1)$。
如果答案被计算两次,每一段各计算一次首尾的贡献,最终再计算一次开头和结尾的贡献。
那么此时一段的贡献为 $(A_l + A_r) (r - l)$,不难发现这个是某个梯形面积的两倍。
不难发现取 ${(i, A_i)}$ 的上凸壳的点的时候,这个面积能达到最大值。(因为显然这个时候 $F_i$ 达到了最大值)
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
typedef class Point {
public:
ll x, y;
Point() { }
Point(ll x, ll y) : x(x), y(y) { }
} Point;
Point operator - (Point a, Point b) {
return Point(a.x - b.x, a.y - b.y);
}
ll cross(Point a, Point b) {
return a.x * b.y - a.y * b.x;
}
int n;
int main() {
scanf("%d", &n);
vector<ll> A (n), B (n);
for (auto& x : A) {
scanf("%lld", &x);
}
for (auto& x : B) {
scanf("%lld", &x);
}
int id = 0;
for (int i = 1; i < n; i++) {
if (A[i] > A[id]) {
id = i;
}
}
rotate(A.begin(), A.begin() + id, A.end());
rotate(B.begin(), B.begin() + id, B.end());
A.push_back(A[0]);
B.push_back(B[0]);
vector<ll> C (n + 1, 0);
for (int i = 2; i <= n; i++) {
C[i] = 2 * (C[i - 1] + B[i - 1]) - C[i - 2];
}
int tp = 0;
vector<Point> stk (n + 3);
for (int i = 0; i <= n; i++) {
Point P (i, A[i] - C[i]);
while (tp >= 2 && cross(stk[tp] - stk[tp - 1], P - stk[tp]) >= 0)
tp--;
stk[++tp] = P;
}
ll res = 0;
for (int i = 2; i <= tp; i++) {
res += (stk[i - 1].y + stk[i].y) * (stk[i].x - stk[i - 1].x);
}
res += stk[1].y + stk[tp].y;
for (int i = 0; i <= n; i++) {
res += 2 * C[i];
}
res -= A[0] * 2;
double ans = 1.0 * res / (2 * n);
printf("%.12lf
", ans);
return 0;
}
Problem F Name-Preserving Clubs
假设有 $k$ 个集合,那么考虑建一个 $k imes n$ 的矩阵,每一位填 0 或者 1,表示这个集合中是否包含这个元素。
首先考虑任意两个集合都不同的情况。
如果称一个上述矩阵是好的,那么当且仅当任意打乱它的列(不能和原来相同),不存在一种方式使得打乱行和最初的矩阵相同。不难发现,这和题目中的一个 name-preserving configuration 一一对应。
不难证明一个好的矩阵任意两列都不同,因为如果存在两列相同,我们交换这两列, 它和原来一模一样,这和定义矛盾。
假设一个矩阵 $A$ 是好的,可以注意到下面两个性质:
- $A$ 的转置 $A^{T}$ 是好的。
- 考虑 $2^k$ 种不同的列,由其中所有不存在于 $A$ 的列构成的矩阵 $A^{C}$ 也是好的。
前者如果不成立,那么对应的行列操作可以应用到 $A$ 上使得操作后和它自己相同。因为没有任意一行或者一列是相同的,所以行列都至少操作 1 次,因此这是满足定义的。
注意到行列操作是独立的,因此先打乱行,再打乱列是等价的。
对于后者,考虑如果不成立,那么打乱行后,使得和原来的列集合相同,可以推出,做这些打乱行操作,可以使得 $A$ 不是好的。
推论 设 $c(k, n)$ 表示本质不同的 $k imes n$ 的好的矩阵的数量,那么有 $c(k, n) = c(n, k), c(k, n) = c(k, 2^{k} - n)$
证明由上述讨论易得。
设 $g(n)$ 表示最小的 $k$ 使得 $c(k, n) > 0$,那么有:
性质1 $2^{g(n)} - n geqslant g(g(n))$
证明 不断应用推论可得 $c(g(n), n) = c(g(n), 2^{g(n)} - n) = c(2^{g(n)} - n, g(n))$,然后由定义可得。
设函数 $G(n)$ 满足 $G(1) = 0$,$G(n)$ 是最小的 $k$ 满足 $2^{k} - n geqslant G(k)$。
引理1 对于 $n > 1$,那么有 $0 leqslant G(i) - G(i - 1) leqslant 1$。
证明 首先不难用归纳法证明 $G(i) < i$。然后 $G(i) geqslant G(i - 1)$ 比较显然,这里略去证明。
考虑用归纳法,当 $n = 2,3$ 的时候显然。
考虑 $n = i(i > 3)$ 的情形。因为 $2^{G(i - 1)} - (i - 1) geqslant G(G(i - 1))$,$2^{G(i - 1)} geqslant 2$,所以有 $2^{G(i - 1) + 1} - i geqslant 2^{G(i - 1)} + 1 - (i - 1) geqslant G(G(i - 1)) + 1 geqslant G(G(i - 1) + 1)$ 。
引理2 对于 $k geqslant G(n)$,那么都有 $2^k - n geqslant G(k)$
证明 考虑用归纳法,当 $k = G(n)$ 显然成立。
当 $k > G(n)$ 的时候因为有 $2^{k - 1} geqslant 1$,所以有 $2^{k} - n geqslant 2^{k -1} + 1 - n geqslant G(k - 1) + 1 geqslant G(k)$。
引理3 $c(k, n) > 0$ 当且仅当 $G(n) leqslant k leqslant 2^n - G(n)$。
证明 必要性显然,考虑充分性。
考虑使用归纳法,当 $n = 1$ 的时候显然成立。下面当 $n > 1$ 的时候
如果 $G(n) leqslant k < n$,那么可以用推论使得变为 $k > n$ 的情形。现在我们来证明它满足条件,因为 $k geqslant G(n)$,所以有 $2^k - n geqslant G(k)$,所以有 $n leqslant 2^n - G(k)$。因为 $k leqslant 2^n - G(n)$,所以 $2^n - k geqslant G(n)$,因此 $n geqslant G(k)$,因此有 $G(k) leqslant n leqslant 2^n - G(k)$。
如果 $2^{n - 1} < k$,那么可以用推论使得变为 $k < 2^{n - 1}$ 的情形。我们还是来证明它满足条件,因为 $2^n - k < k$,所以只用证明 $G(n) leqslant 2^n - k$。因为 $2^n - G(n) geqslant k$,移项可得它成立。
现在考虑 $n leqslant k leqslant 2^{n - 1}$。
考虑构造一个集合 ${{1}, {1, 2}, {2, 3}, {3, 4}, cdots, {n - 1, n}}$。然后对于剩下 $k - n$ 个填任意大小大于等于 $3$ 的集合。
可以手动验证当 $n = 2, n = 3$ 可行,当 $n geqslant 4$ 的时候,满足大小大于等于 $3$ 的集合至少占了一半,因此一定可行。
引理4 当 $6 leqslant k leqslant n leqslant 2^{k - 1}$ 时, $c(k, n) > 1000$
证明的思路大概是考虑先构造一个大小为 $k - 2$ 的集合,包含 ${1, 2}, {2, 3}, cdots, {k - 2, k -1}$,然后将其中一个或者其中 $k - 3$ 个取补集。剩下的 $n - k + 2$ 个集合随便塞大小不为 2 或者 $k - 2$ 的集合。
然后考虑两个集合可能相同的情况。由于 yyf 非常菜,目前还不会,所以咕咕咕咕。结论是 $n = 4$ 的时候答案加上 1,$n = 7$ 的时候答案加上 2.
对于剩下 $k leqslant 5, n leqslant 2^{k - 1}$ 的情况写一个爆搜就行了。如果您的爆搜比较慢,请打表。
Code
#include <bits/stdc++.h>
using namespace std;
/*
#include <bits/stdc++.h>
using namespace std;
const int Nmx = 5;
const int fac[6] = {1, 1, 2, 6, 24, 120};
int shuf[Nmx + 1][120][1 << Nmx];
void prepare() {
auto arrange = [&] (int v, int l, const vector<int>& p) {
int rt = 0;
for (auto x : p) {
if (v & 1) {
rt |= 1 << x;
}
v >>= 1;
}
return rt;
};
for (int l = 1; l <= Nmx; l++) {
vector<int> p (l);
for (int i = 0; i < l; i++) {
p[i] = i;
}
for (int id = 0; next_permutation(p.begin(), p.end()); id++) {
for (int i = 0; i < (1 << l); i++) {
shuf[l][id][i] = arrange(i, l, p);
}
}
}
}
bool check(int n, const vector<int>& tb) {
static int vis[1 << Nmx], dfc = 0;
++dfc;
for (auto x : tb) {
vis[x] = dfc;
}
for (int i = 0; i < fac[n] - 1; i++) {
bool flag = true;
for (int j = 0; j < (signed) tb.size() && flag; j++) {
flag = vis[shuf[n][i][tb[j]]] == dfc;
}
if (flag) {
return false;
}
}
return true;
}
int ans = 0;
vector<int> stk;
void dfs(int l, int d, int k, int ls) {
if (d == k) {
ans += check(l, stk);
if (ans > 1000 * fac[l]) {
throw 1;
}
return;
}
for (int s = ls + 1; s < (1 << l); s++) {
stk[d] = s;
dfs(l, d + 1, k, s);
}
}
int n, k;
int table[6][20];
int main() {
prepare();
for (int k = 1; k <= 5; k++) {
for (int n = k; n <= (1 << (k - 1)); n++) {
::k = k;
::n = n;
::ans = 0;
stk.resize(n);
try {
dfs(k, 0, n, -1);
} catch(int) {
ans = 1001 * fac[k];
}
cout << k << " " << n << '
';
ans /= fac[k];
table[k][n] = ans;
}
}
cout << "{{}";
for (int i = 1; i <= 5; i++) {
cout << ",
{";
cout << table[i][0];
for (int j = 1; j <= 16; j++) {
cout << ", " << table[i][j];
}
cout << "}";
}
cout << "};
";
return 0;
}
*/
const int table[6][20] = {{},
{1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 4, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 36, 108, 220, 334, 384, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 976, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001}};
#define ll long long
vector<int> G (105);
void prepare() {
G[1] = 0;
for (int n = 2; n <= 100; n++) {
for (int k = 1; k < n; k++) {
if ((1 << k) >= n && ((1 << k) - n) >= G[k]) {
G[n] = k;
break;
}
}
}
}
int g(ll n) {
if (n <= 100) {
return G[n];
}
for (int k = 1; ; k++) {
if ((1ll << k) >= n && ((1ll << k) - n) >= g(k)) {
return k;
}
}
assert(false);
return -1;
}
int solve(ll n, ll k) {
if (n < k) {
swap(n, k);
}
if (k < 63 && (1ll << k) - n < n) {
return solve((1ll << k) - n, k);
}
if (k > 5) {
return 1001;
}
return k == 0 ? 1 : table[k][n];
}
int main() {
ll n;
prepare();
scanf("%lld", &n);
int ans = solve(n, g(n));
ans += (n == 4);
ans += (n == 7) * 2;
printf("%d
", (ans > 1000) ? -1 : ans);
return 0;
}