AtCoder Grand Contest 044 简要题解

从这里开始

  因为比赛的时候在路上,所以又成功错过下分和被神仙 jerome_wei 吊起来打(按在地上摩擦)的好机会。

Problem A Pay to Win

  把这个过程倒过来。不难发现到下一次除之前,要么是加到 $\lfloor n/d \rfloor d$ 要么是 $\lceil n/d \rceil d$,或者直接减到 0.

  直接用 map 记忆化的复杂度为 $O(T \log^3 N \log \log N)$。具体的来说,每个状态只可能是 $\lfloor \frac{n}{2^a3^b5^c}\rfloor$ 或者 $\lceil\frac{n}{2^a3^b5^c}\rceil$,对于 $ \left\lceil\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rceil  $ 的情况有 $\left \lfloor\frac{n}{uv} \right \rfloor = \left\lfloor\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rfloor \leqslant \left\lceil\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rceil  \leqslant \left\lceil\frac{\lceil\frac{n}{u}\rceil}{v}\right\rceil = \left\lceil\frac{n}{uv}\right\rceil$ 

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) << '\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\n", 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 << '\n';
	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 << '\n';
	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\n", ans);
	return 0;
}

Problem F Name-Preserving Clubs

  假设有 $k$ 个集合,那么考虑建一个 $k \times n$ 的矩阵,每一位填 0 或者 1,表示这个集合中是否包含这个元素。

  首先考虑任意两个集合都不同的情况。

  如果称一个上述矩阵是好的,那么当且仅当任意打乱它的列(不能和原来相同),不存在一种方式使得打乱行和最初的矩阵相同。不难发现,这和题目中的一个 name-preserving configuration 一一对应。

  不难证明一个好的矩阵任意两列都不同,因为如果存在两列相同,我们交换这两列, 它和原来一模一样,这和定义矛盾。

  假设一个矩阵 $A$ 是好的,可以注意到下面两个性质:

  • $A$ 的转置 $A^{T}$ 是好的。
  • 考虑 $2^k$ 种不同的列,由其中所有不存在于 $A$ 的列构成的矩阵 $A^{C}$ 也是好的。

  前者如果不成立,那么对应的行列操作可以应用到 $A$ 上使得操作后和它自己相同。因为没有任意一行或者一列是相同的,所以行列都至少操作 1 次,因此这是满足定义的。

  注意到行列操作是独立的,因此先打乱行,再打乱列是等价的。

  对于后者,考虑如果不成立,那么打乱行后,使得和原来的列集合相同,可以推出,做这些打乱行操作,可以使得 $A$ 不是好的。

推论 设 $c(k, n)$ 表示本质不同的 $k \times 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 << '\n';
      ans /= fac[k];
      table[k][n] = ans;
    }
  }
  cout << "{{}";
  for (int i = 1; i <= 5; i++) {
    cout << ",\n {";
    cout << table[i][0];
    for (int j = 1; j <= 16; j++) {
      cout << ", " << table[i][j];
    }
    cout << "}";
  }
  cout << "};\n";
  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\n", (ans > 1000) ? -1 : ans);
  return 0;
}

posted @ 2020-05-25 23:06  阿波罗2003  阅读(566)  评论(0编辑  收藏  举报