Ad-hoc 训练
难度 \([1, 10]\),\(\le 5\) 为秒杀,\(6\) 思考 \(\le \texttt{30min}\),\(7\) 勉强做出来(1h)。\(8\) 是做不出来但是可能认为不是很难的题目。 \(9\) 是基本上现在水平想不出来的题目但是可以为题解做法给出自然推导的,\(10\) 是看懂题解都很困难的题目。
思考时间: 应当 在 \([40, 80] \texttt{min}\) 左右,不应该 \(\le \texttt{30min}\)。
代码实现:肯定都要写一遍,但是不一定是现在。
ARC155D Avoid Coprime Game
-
Tag: 思维 + 一定套路
-
Diffculty: 7+
不是各门,就是说为啥我没做出来。
AGC027F Grafting
-
Tag: 思维链长 + 突破口困难
-
Diffculty: 8
特判掉 \(0\) 次操作(长一样),此时操作次数至少为 \(1\)。
首先考虑定一个根,我们枚举第一次操作 \((u, v)\) 表示 \(u\) 接上 \(v\) 并且锁定了 \(u\)。那么把 \(u\) 作为根即可。那么可以操作的只可能是当前有根树中的叶子。令 \(x\) 在 \(A, B\) 树中的父亲分别为 \(Afa_x, Bfa_x\),则对于 \(Afa_x = Bfa_x\) 的节点,显然不需要操作也不能操作。所以我们要操作的节点已经确定了,也就是操作次数已经确定了,我们只需要考虑是否存在一种合法方案。那么不能操作的节点一定在根处形成一个连通块,然后忽略掉这些点。注意到在 \(A\) 树中 \(x\) 的操作肯定是要比父亲更早的,同理,\(B\) 中的限制类似,之间连一条有向边来限制先后关系,然后判断是否有环即可。
时间复杂度 \(O(Tn^3)\)。
qwq
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
#define ull unsigned long long
#define umap unordered_map
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10;
const ll mod = 998244353;
/*
struct edge{
int v, next;
}edges[M << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
*/
il ll qpow(ll x, ll y){
ll ret = 1;
for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmod(ll& x){x = (x + mod) % mod;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}
//#define int long long
int n, Afa[N], Bfa[N], da[N], deg[N];
vector<int> A[N], B[N], G[N];
bool op(int x){return Afa[x] != Bfa[x];}
void dfsa(int u, int fa){
Afa[u] = fa;
for(auto v : A[u]) if(v != fa) dfsa(v, u);
}
void dfsb(int u, int fa){
Bfa[u] = fa;
for(auto v : B[u]) if(v != fa) dfsb(v, u);
}
void solve(){
cin >> n; int ans = n + 1;
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
A[x].pb(y); A[y].pb(x); da[x]++; da[y]++;
}
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
B[x].pb(y); B[y].pb(x);
} dfsa(1, 0); dfsb(1, 0);
bool gg = 1;
for(int i = 1; i <= n; i++) if(Afa[i] != Bfa[i]){gg = 0; break;}
if(gg){cout << 0 << "\n"; return;}
for(int u = 1; u <= n; u++) if(da[u] == 1){
dfsa(u, 0); dfsb(u, 0);
for(int v = 1; v <= n; v++){
if(u == v) continue; int rel = A[u][0];
for(vector<int>::iterator it = A[rel].begin(); it != A[rel].end(); it++){
if((*it) == u){A[rel].erase(it); break;}
} A[u].clear(); A[u].pb(v); A[v].pb(u);
dfsa(u, 0); int ret = 0, qwq = 0;
for(int i = 1; i <= n; i++) deg[i] = 0, G[i].clear();
for(int i = 1; i <= n; i++){
ret += op(i);
if(op(Afa[i]) && (!op(i))){qwq = 1; break;}
else if(op(i) && op(Afa[i])) G[i].pb(Afa[i]), deg[Afa[i]]++;
if(op(Bfa[i]) && (!op(i))){qwq = 1; break;}
else if(op(i) && op(Bfa[i])) G[Bfa[i]].pb(i), deg[i]++;
}
queue<int> Q; int tc = 0;
for(int i = 1; i <= n; i++) if(!deg[i] && op(i)) Q.push(i);
while(!Q.empty()){
int u = Q.front(); Q.pop(); tc++;
for(auto v : G[u]){
deg[v]--;
if(deg[v] == 0) Q.push(v);
}
}
if((!qwq) && tc == ret) chkmin(ans, ret + 1);
A[u].clear(); A[u].pb(rel); A[rel].pb(u);
A[v].pop_back();
}
}
if(ans == n + 1) cout << -1 << "\n";
else cout << ans << "\n";
}
void clr(){
for(int i = 1; i <= n; i++) A[i].clear(), B[i].clear(), da[i] = 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T = 1; cin >> T; while(T--) solve(), clr();
return 0;
}
ARC156C Tree and LCS
-
Tag: 观察上下界 + 构造
-
Difficulty: 7+
首先有一个基本的观察,就是最大的相似度只可能在叶子到叶子的路径产生,并且最大相似度的下界为 \(1\)。这是因为我们可以构造 \(i \to P_i\) 的路径。然后我们观察到我们无论如何都可以取到这个下界。这里我做了一个转化,而且差一点就做出来了。就是注意到编号是不重要的,那么不妨重新编号一下,让任意的 \(fa_i < i\),然后再让 \(P_{fa_i} < P_i\)。继续转化成 \(\rm LCS\) 变成 \(\rm LIS\)。那么假设我们的路径是 \(u \to c \to v\),其中 \(c = \operatorname{LCA}(u, v)\),那么 \(u \to c\) 的路径上的 \(P\) 贡献至多为 \(1\)。然后我们这么构造:
-
随便找一个非叶子节点作为根节点,并且从叶子节点开始一层一层的剥离,每次配对叶子 \((u, v)\) 并且让 \(P_u = v\), \(P_v = u\)。
-
假设节点个数为奇数,就令根节点的 \(P\) 等于自身。
此时容易发现 \(c \to v\) 上的下标数组为单调下降的,于是不可能产生大于 \(1\) 的贡献。
实现不需要找到根节点,时间复杂度 \(O(n^2)\)。瓶颈在 checker。
[JOI Open 2021] 怪兽游戏 / Monster Game
-
Tag: 交互,性质
-
Difficulty: 7+
思考了差不多 2h,讲一下思考过程。
称 \(i\) 可以打败 \(j\) 当且仅当 \(f(i, j) = 1\)。
先考虑 \(n^2\) 的暴力如何做:对于每个 \(i\),求出 \(rk_i = \sum_{i = 0}^{n - 1}[i \ne j] f(i, j)\)。那么不难发现除了在排列中除了 \(p_i = 0/n - 1\) 的位置 \(i\) 都有 \(p_i = rk_i\),这是因为对于一个 \(i\),若 \(i - 1, i + 1\) 均存在,那么此时 \(i\) 可以打败 \(i + 1\),而不能打败 \(i - 1\),两者算错的贡献恰好抵消了。而对于 \(p_i = 0\),我们会算出来 \(rk_i = 1\),因此 \(rk\) 中会有两个 \(1\),多比较一次即可算出哪个是真正的 \(1\)。对于 \(rk_i = n - 2\) 的同理算出哪个才是真正的 \(n - 2\)。那么此时我们有了 \(\frac{(n - 1)n}{2} + 2\) 次数的做法。
我们考虑如何优化。发现本质上我们要求的东西就是一个值的排名,那么我们直接排个序,这样是不是就做完了?实则并非,考虑如下样例:
5
1 0 2 4 3
手玩一下你会发现,在上面这个样例中,对于每一个 \(p_{i + 1}\) 都可以打败 \(p_i\),但是写出 \(rk\) 数组发现完全是错的。而且假如你写了把 query 作为 cmp 传入 sort 中来排序,你会发现交上去 RE 了。这两个的具体原因都是,这些大小关系并不满足传递性,换句话说,假设 \(i\) 可以打败 \(j\) 就连边 \(i \to j\),则该图中有若干三元环 \(x - 1\to x\to x + 1 \to x - 1\)。
下面设排好序的(下标)序列为 \(id_0, id_1, ..., id_{n - 1}\),此时满足 \(\forall 0 \le i \le n - 2,f(id_i, id_{i + 1}) = 1\),同时设 \(p'_i = p_{id_i}\)。
但是当我们研究完这个冲突的本质后,我们发现,\(p'_i,p'_{i + 1}\) 一定满足 \(p'_i < p'_{i + 1}\) 或者 \(p'_i = p'_{i + 1} + 1\)。对着这个东西再想一想,发现我们可以将 \(p'\) 划分为若干个在下标上和值域上均连续的一些区间。形式化的,我们可以将 \(p\) 划分为若干个区间 \([l_1, r_1], [l_2, r_2], [l_3, r_3]..., [l_m, r_m]\),且满足以下性质:
-
\(\forall i \in [1, m], j \in [l_i, r_i)\),有 \(p'_{j} = p'_{j + 1} + 1\)。
-
\(\forall i \in [1, m)\),有 \(l_i = r_{i - 1} + 1, p'_{r_i} = p'_{l_{i - 1}} + 1\)。
那么问题转化为求出所有的区间,考虑增量法。假设我们求出了上一个区间 \([l_{i - 1}, r_{i - 1}]\),则 \(l_i = r_{i - 1} + 1\),然后我们对于 \(j \ge l_i\) 逐个判定 \(j\) 是否是当前区间的元素。但是这样不是很好做,不妨转化一下,判断 \(j\) 是否为当前区间的结尾。那么根据 \(p'_{r_i} = p'_{l_i} + 1\) 以及 \(j \ge l_i\),我们可以发现,当且仅当 \(j = r_i\) 时,\(f(id_{l_{i - 1}}, id_j) = 1\)。依赖这个性质,我们就可以求出除了第一个以外的区间了。
那么如何求出第一个区间呢?注意到我们只需要求出 \(p'_0\) 即可,那么直接使用暴力即可。同时注意求出来的 \(p'_0 = 1/n - 2\) 的情况,由于我们的暴力不区分 \(0/1, n-2/n-1\),此时需要再求出 \(p'_1\) 才可以判断 \(p'_0\) 到底是什么。
最劣查询次数是 \(n \log n + 3n\)。会略微会超过 \(10000\) 一点点,但是由于取平均值的原因,依然可以获得 100 分。
100 pts 代码。
qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
#define ull unsigned long long
#define umap unordered_map
using namespace std;
const int N = 1000 + 10, M = 2e5 + 10;
const ll mod = 998244353;
/*
struct edge{
int v, next;
}edges[M << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
*/
il ll qpow(ll x, ll y){
ll ret = 1;
for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmod(ll& x){x = (x + mod) % mod;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}
//#define int long long
extern "C" bool Query(int a, int b);
int n, rk[N], id[N], tmp[N];
void mysort(int l, int r){
if(l >= r) return;
int mid = (l + r >> 1), j = mid + 1, now = l;
mysort(l, mid); mysort(mid + 1, r);
for(int i = l; i <= mid; i++){
while(j <= r && Query(id[i], id[j])) tmp[now] = id[j], j++, now++;
tmp[now] = id[i]; now++;
}
while(j <= r) tmp[now] = id[j], j++, now++;
for(int i = l; i <= r; i++) id[i] = tmp[i];
}
// can't distinguish(?) 0 / 1, n - 2 / n - 1
int getid(int x){
int gt = 0;
for(int i = 0; i < n; i++) if(i != x) gt += Query(x, i);
return gt;
}
vector<int> ans, res;
void solve(int st){
int lstfir = 0, lst = st;
for(int i = st; i < n; i++){
if(!Query(id[i], id[lstfir])){
int cnt = i;
for(int j = lst; j <= i; j++) ans[j] = cnt, cnt--;
lstfir = lst; lst = i + 1;
}
}
}
extern "C" std::vector<int> Solve(int N){
bool dbg = 0;
n = N; ans.resize(n); res.resize(n);
for(int i = 0; i < n; i++) id[i] = i;
mysort(0, n - 1);
for(int i = 0; i < n; i++) ans[i] = id[i];
if(dbg) return ans;
// check
//for(int i = 0; i < n; i++) cerr << id[i] << " "; cerr << "\n";
// for(int i = 0; i < n - 1; i++) assert(Query(id[i + 1], id[i]));
// solve
int a0 = getid(id[0]);
if(a0 == 1){
if(getid(id[1]) == 1) ans[0] = 1, ans[1] = 0, solve(2);
else ans[0] = 0, solve(1);
} else if(a0 == n - 2){
if(getid(id[1]) == n - 2){
for(int i = 0; i < n; i++) ans[i] = n - i - 1;
} else{
for(int i = 0; i < n - 1; i++) ans[i] = n - i - 1;
ans[n - 1] = n - 1;
}
} else{
for(int i = 0; i <= a0; i++) ans[i] = a0 - i;
solve(a0 + 1);
}
for(int i = 0; i < n; i++) res[id[i]] = ans[i];
return res;
}
JOI Open 2024 图书馆 3 / Library 3
-
Tag: 交互 + 增量
-
Difficulty: 8-
核心:通过整合多个信息询问来减小询问次数。(尽量利用询问给予的信息)
感觉只差一步,但是死活想不出来。
不难发现询问告诉你的信息实际上是当前排列的置换环个数。那么如何判断 \(p_x, p_y\) 是否在同一个置换环上?直接询问出原来的环的个数 \(m\),然后交换 \(p_x, p_y\) 再次查询,如果环的个数 \(+1\),则 \(p_x, p_y\) 在同一个环上,反之如果 \(-1\) 就不在同一个环上。考虑增量构造,假设现在满足 \(p_0, p_1, ..., p_{i - 1}\) 已经满足他们都不在一个环上,考虑加入 \(p_i\)。则此时至多有一个 \(j < i\) 满足 \(p_j\) 和 \(p_i\) 在同一个环上,暴力找的询问次数是 \(\frac{(n - 1)n}{2} + 1\) 的。随机化也过不了。
考虑二分,问题转化为对于 \(j < i\),判断 \(p_0, p_1, ..., p_j\) 里面是否有一个与 \(p_i\) 处于同一个环中。那么我们考虑将 \(p_0, p_1, ..., p_j, p_i\) 全部合并到一个环里面,然后查询前后环的变化量,假设 \(p_i\) 与某个 \(p_k\) 是同一个环的,那么变化量就是 \(+j\),反之 \(+(j - 2)\)。具体构造可以让 \(\forall 1 \le k \le j, p'_{k - 1} = p_k, p'_j = p_i, p'_i = p_0\)。
询问次数 \(n \log n\),足以通过。
qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
#define ull unsigned long long
#define umap unordered_map
using namespace std;
const int N = 500 + 10, M = 2e5 + 10;
const ll mod = 998244353;
/*
struct edge{
int v, next;
}edges[M << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
*/
il ll qpow(ll x, ll y){
ll ret = 1;
for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmod(ll& x){x = (x + mod) % mod;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}
//#define int long long
void answer(std::vector<int>);
int query(std::vector<int>);
vector<int> p;
int n;
int Query(std::vector<int> p){return n - query(p);}
void solve(int N){
n = N; p.resize(n);
for(int i = 0; i < n; i++) p[i] = i;
for(int i = 1; i < n; i++){
int rel = Query(p), l = 0, r = i - 1, ans = i;
while(l <= r){
int mid = (l + r >> 1);
vector<int> pp(p);
for(int i = 1; i <= mid; i++) pp[i - 1] = pp[i];
pp[mid] = p[0]; swap(pp[mid], pp[i]);
if(Query(pp) == rel - mid + 1) r = mid - 1, ans = mid;
else l = mid + 1;
} //cerr << ans << "\n";
if(ans != i) swap(p[ans], p[i]);
} answer(p);
}
CF2066A Object Identification
-
Difficulty: ?
-
Tag: *1400,观察与对比
相当于找不同。首先发现假设我们有一个 \(x \in \{1, 2, ..., n\}, x \notin \{x_1, x_2, ..., x_n\}\),那么询问 \((x, y)\) 即可,如果答案是 \(0\),则是 \(A\),否则是 \(B\)。那么现在只有可能 \(X\) 是一个排列,那么这个时候它是一棵外向基环树,然后如果这个时候随便找两个点 \(x, y\) 并正反都询问一次,如果不相同显然是 \(A\),但是相同时我们不能区分 \(x, y\) 的连线恰好是环的直径。所以我们必须再询问 \((x, z)\) 和 \((z, x)\),然后这里是 \(4\) 次询问。但是我们实际上没有好好利用 \(x, y\) 的询问给出来的信息,注意到假设相等时是答案 \(A\),那么询问的答案必然不超过 \(\frac{n}{2}\),也就是说假设我们选的点是 \(1, n\),则此时可以区分答案,询问次数为 \(2\)。

浙公网安备 33010602011771号