模拟赛 R23
T1 - 星际、干涉、融解、轮回、邂逅、再生、安魂曲
题目描述
给定正整数 \(n\),求有多少三元组 \((a,b,c)\) 满足:
- \(a+b+c=n\)
- \(1\le a<b<c\le n\)
- \(a\) 是 \(b\) 的因数,\(b\) 是 \(c\) 的因数
\(n \le 10^9\)
Solution
枚举 \(a\),显然 \(a | n\)。设 \(b = xa, c = xya\),那么 \(x \times (y + 1) = \frac{n}{a} - 1\)。此时我们再枚举一遍 \(x\) 即可,注意一些 corner 因为 \(x > 1\) 和 \(y > 1\)。约数个数在 \(n\) 很大时有更加精确的上界 \(n^{\frac{1}{3}}\)。因此直接枚举复杂度不会爆,当然线性筛筛出来然后单次 \(O(\frac{\sqrt n}{\ln n})\) 也无所谓。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 5;
bitset<N> bs;
int pri[N], cnt;
int num(int T){
if(T <= 2) return 0;
int tmp = -2 - !(T & 1), ret = 1;
for(int i = 1; i <= cnt; ++i){
if(T % pri[i] == 0){
int cnt = 0;
while(T % pri[i] == 0){
T /= pri[i];
++cnt;
}
ret *= (cnt + 1);
}
}
if(T != 1) ret *= 2;
return ret + tmp;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
for(int i = 2; i <= 5e4; ++i){
if(!bs[i]) pri[++cnt] = i;
for(int j = 1; j <= cnt; ++j){
if(i * pri[j] > 5e4) break;
bs[i * pri[j]] = 1;
if(i % pri[j] == 0) break;
}
}
int n; cin >> n;
int ans = 0;
for(int i = 1; i <= n / i; ++i){
if(n % i != 0) continue;
ans += num(n / i - 1);
if(i != n / i){
ans += num(i - 1);
}
}
cout << ans;
return 0;
}
T2 - 临界、自毁、客体、重生、而你、又在、何处呢
题目描述
二维平面上有 \(n\) 个点 \((x,y)\),且 \(1\le x,y\le N\),每个点是黑色或白色。
对于一个矩形(可以退化为线段或单点),定义其权值为其中黑点个数与白点个数的乘积。
现在让矩形的上下和左右边界取遍 \(1\sim N\),求所有 \(\left(\frac{N(N+1)}{2}\right)^2\) 个矩形的权值和。
答案对 \(2^{64}\) 取模。
\(n \le 2 \times 10^5, N \le 10^9\)
Solution
考虑一对黑点和白点何时会被几个矩形统计到。假设黑点是 \((x, y)\),白点是 \((x', y')\)。那么就会被 \(min(x', x) \times (N - \max(x', x) + 1) \times min(y, y') \times (N - \max(y, y'))\) 统计到。然后分讨做二位数点即可。
赛后才做出一个更劣解,对 \(y\) 轴扫描线,令 \(f_i\) 表示 \(xl \le x \land xr \ge x\) 的所有矩形内黑点的数量之和。这样我们在黑点处加贡献,白点处计算即可,随后是一段区间加,维护 \(ki + b\) 状物。本质相同的。
Code
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 2e5 + 5, M = N << 2;
int n, V, tot;
ull a[N];
struct BIT{
ull tr[M];
int lowbit(int x){ return x & (-x); }
void clear(){ memset(tr, 0, sizeof(tr)); }
void add(int x, ull k){
for(int i = x; i <= tot; i += lowbit(i))
tr[i] += k;
}
ull qry(int x){
ull res = 0;
for(int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
}k, b;
int get(int x){ return lower_bound(a + 1, a + 1 + tot, x) - a; }
void add(int L, int R, ull kd, ull bd){
k.add(L, kd), k.add(R + 1, -kd);
b.add(L, bd), b.add(R + 1, -bd);
}
ull qry(int x){
return a[x] * k.qry(x) + b.qry(x);
}
map<int, vector<pii> > mp;
int main(){
// freopen("peace5.in", "r", stdin);
// freopen(".out", "w", stdout);
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> V;
for(int i = 1; i <= n; ++i){
int x, y, c;
cin >> x >> y >> c;
mp[y].emplace_back(c, x);
a[++tot] = x;
}
a[++tot] = 1, a[++tot] = 1e9 + 1;
sort(a + 1, a + 1 + tot);
tot = unique(a + 1, a + 1 + tot) - a - 1;
ull ans = 0;
for(auto it = mp.begin(); it != mp.end(); ++it){ // le y
auto &[y, p] = *it;
sort(p.begin(), p.end(), greater<pii>());
for(auto [c, x] : p){
x = get(x);
// cout << x << ' ' << a[x] << ' ' << y << '\n';
if(!c) ans += (V - y + 1) * qry(x);
else{
add(1, x, (V - a[x] + 1) * y, 0);
add(x + 1, tot, -a[x] * y, ull(V + 1) * a[x] * y);
}
}
// cout << y << ' ' << ans << '\n';
}
k.clear(), b.clear();
for(auto it = mp.rbegin(); it != mp.rend(); ++it){ // > y
auto &[y, p] = *it;
sort(p.begin(), p.end());
for(auto [c, x] : p){
x = get(x);
if(!c) ans += y * qry(x);
else{
add(1, x, (V - a[x] + 1) * (V - y + 1), 0);
add(x + 1, tot, -a[x] * (V - y + 1), ull(V + 1) * a[x] * (V - y + 1));
}
}
// cout << y << ' ' << ans << '\n';
}
cout << ans;
return 0;
}
T3 - 向我施展咒語吧 神明大人!
题目描述
有 \(n\) 个区间 \([l_i,r_i]\),其中 \(1\le l_i<r_i\le m\)。
现在会进行若干次切割操作,每次会给出 \(x,s,t\),表示对 \(s\le i\le t\) 的区间 \([l_i,r_i]\) 从 \(x\) 处切割,保留较长的一段,两段相等时保留左侧的。具体的:
- 如果 \(x< l_i\) 或者 \(x> r_i\),则无事发生。
- 如果 \(r_i\ge x\ge \frac{l_i+r_i}{2}\),则令 \(r_i\leftarrow x\)。
- 如果 \(l_i\le x<\frac{l_i+r_i}{2}\),则令 \(l_i\leftarrow x\)。
求所有切割操作结束后,每个区间的左右端点。保证所有切割操作的位置 \(x\) 恰好构成 \(1\sim m\) 的排列。
\(n \le 10^5, m \le 10^6\)
Solution
由于切割位置形成一个排列,那么记 \(p_i\) 表示切 \(i\) 这一刀是第几个操作,如果我们想知道一个区间第一次被切的那一刀,直接查询 \(\min p_{l\cdots r}\) 即可。我们直接模拟这个过程,可以获得一些分数。
现在的问题是对于每个线段,切割的次数有可能很大,可能卡到 \(O(n)\) 级别。
我们对每个线段三等分,我们发现当切割位置落在左边/右边时,我们很容易地判断应该丢掉哪边。这是由于中间的长度总是不小于两边的长度。而对于切到中间的操作,我们发现这至少会使整个线段长度 \(\times \frac{2}{3}\)。因此对于每一轮操作,我们先找到中间那一段第一次被切的一刀 \(i\),然后向两边找。找到左边最右的那个 \(j\),满足 \(p_j < p_i\),和右边最左的那个。这样我们就知道在切 \(i\) 之前,整个线段的形态了,然后直接模拟切 \(i\) 这刀会丢到哪边。这样的轮数是不会超过 \(O(\log m)\) 的。
对于 \((s, t)\) 的限制,我们对线段编号一维扫面线即可。这样要维护的就是单点修改,区间最小值和线段树二分。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef tuple<int, int, int> tpi;
typedef pair<int, int> pii;
int n, m, l[N], r[N], id, inf;
vector<pii> op[N];
struct Seg{
int tr[N << 2];
#define ls(p) p << 1
#define rs(p) p << 1 | 1
void upd(int x, int k, int p = 1, int pl = 1, int pr = m){
if(pl == pr) return tr[p] = k, void();
int mid = (pl + pr) >> 1;
if(x <= mid) upd(x, k, ls(p), pl, mid);
else upd(x, k, rs(p), mid + 1, pr);
tr[p] = min(tr[ls(p)], tr[rs(p)]);
}
vector<tpi> rge;
void get(int L, int R, int p = 1, int pl = 1, int pr = m){
if(L <= pl && R >= pr) return rge.emplace_back(p, pl, pr), void();
int mid = (pl + pr) >> 1;
if(L <= mid) get(L, R, ls(p), pl, mid);
if(R > mid) get(L, R, rs(p), mid + 1, pr);
}
pii qry(int L, int R){
rge.clear(); get(L, R);
int mn = inf;
for(auto [p, _, __] : rge) mn = min(mn, tr[p]);
if(mn == inf) return {-1, mn};
for(auto [p, pl, pr] : rge){
if(tr[p] == mn){
while(pl != pr){
int mid = (pl + pr) >> 1;
if(tr[ls(p)] == mn) p = ls(p), pr = mid;
else p = rs(p), pl = mid + 1;
}
return {pl, mn};
}
}
}
int find_last(int L, int R, int x){
rge.clear(); get(L, R);
for(int i = rge.size() - 1; i >= 0; --i){
auto [p, pl, pr] = rge[i];
if(tr[p] < x){
while(pl != pr){
int mid = (pl + pr) >> 1;
if(tr[rs(p)] < x) pl = mid + 1, p = rs(p);
else pr = mid, p = ls(p);
}
return pl;
}
}
return L;
}
int find_first(int L, int R, int x){
rge.clear(); get(L, R);
for(auto [p, pl, pr] : rge){
if(tr[p] < x){
while(pl != pr){
int mid = (pl + pr) >> 1;
if(tr[ls(p)] < x) pr = mid, p = ls(p);
else pl = mid + 1, p = rs(p);
}
return pl;
}
}
return R;
}
}tr;
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> m >> id;
memset(tr.tr, 0x3f, sizeof(tr.tr));
inf = tr.tr[0];
for(int i = 1; i <= n; ++i) cin >> l[i] >> r[i];
for(int i = 1; i <= m; ++i){
int x, L, R;
cin >> x >> L >> R;
op[L].emplace_back(x, i);
op[R + 1].emplace_back(x, inf);
}
for(int i = 1; i <= n; ++i){
for(auto [x, k] : op[i]) tr.upd(x, k);
int pl = l[i], pr = r[i];
while(pl + 1 != pr){
int ml = pl + (pr - pl + 1) / 3, mr = pr - (pr - pl + 1) / 3;
auto [k, v] = tr.qry(ml, mr);
pl = tr.find_last(pl, ml, v);
pr = tr.find_first(mr, pr, v);
int mid = (pl + pr + 1) >> 1;
if(k == -1) break;
if(k >= mid) pr = k;
else pl = k;
}
cout << pl << ' ' << pr << '\n';
}
return 0;
}
T4 - 最后看到的是那样的梦
题目描述
有一张 \(n\) 个点的无向图,初始图中没有任何边。
接下来不断重复以下操作:
- 设 \(\deg_i\) 为第 \(i\) 个点当前的度数(即有多少个点和它有直接连边),从所有满足 \(\deg_u,\deg_v\) 中至少有一个是 \(0\) 的点对 \((u,v)\) 中等概率选取一个(例如,假设当前有 \(k\) 个点的度数非零,则一共有 \(\binom{n}{2}-\binom{k}{2}\) 种可能选取的点对),然后加一条边 \((u,v)\)。
最终无法进行操作,即不存在孤立点时停止操作。
给定 \(m\),求恰好 \(m\) 次操作后停止的概率,答案对给定的质数 \(P\) 取模。
$ n \le 10^7$
Solution
首先,可以对原题意转化。我们称一次选的两个点一个是非孤立点,一个是孤立点的为一操作,选的两个点都是孤立点的是二操作,那么最终操作的次数就是 \(n\) - 二操作的次数。现在我们可以随机一个长度为 \(n \choose 2\) 的所有边的排列。如果我们碰到一个不合法的边,就跳过。否则就执行该操作。不难发现,这与原题意等价,因为接下来每条合法的边第一次出现的概率都是一样的。
现在令 \(k = n - m\),我们要求 \(({n \choose 2})!\) 种排列中恰好执行 \(k\) 次 2 操作的概率。先容斥一下,接下来我们求至少有 \(x\) 次 2 操作的概率。
先把这 \(x\) 条边选出来,是 \({n \choose 2x}(2x - 1)!!\)。其中涉及到一个经典组合,给定 \(2x\) 个点,求他们两两配对成 \(x\) 条边的方案数(不计边集内的顺序)。那么第一个点有 \(2x - 1\) 个点可以与之配对,剩下 \(2x - 2\) 个点中编号最小的点又有 \(2x - 3\) 个可以跟他配对,最后就是 \((2x - 1)(2x - 3) \cdots 1 := (2x - 1)!!\)。
然后发现变成了一个拓扑序计数的问题。我们先为选出来的这 \(x\) 条边强行钦定一个拓扑序(也就是乘一个 \(x!\))。然后限制就是所有与它相邻的边都不能在它之前出现。不妨设这 \(x\) 条边的拓扑序是 \((0, 1)\to(2, 3) \cdots \to(2x - 2, 2x - 1)\)。对于一条边 \((i, j)\),如果两个点都不在这 \(2x\) 点之内,那显然对他们没有要求。否则,我们找到 \((i, i \oplus 1)\),\((j, j \oplus 1)\) 更靠后的那一个(如果不在 \(2x\) 点之内就选另一个)作为父亲。发现对于这 \(2x\) 个点和它所有相邻的边,形成了一棵树的结构。这棵树的主干就是这 \(x\) 条边,每个点又附着一些别的边,而除了这主干上的 \(x\) 条边,别的都是叶子节点。
为了计数树的拓扑序,我们研究一下子树大小。与 \((2x - 2, 2x - 1)\) 相邻的边有 \(2(n - 2)\),同理与 \((2x - 4, 2x - 3)\) 相邻但不与 \((2x - 2, 2x - 1)\) 相邻的边有 \(2(n - 4)\)。以此类推,从下至上第 \(i\) 个点的子树大小就是 \(i + \sum_{j \le i} 2(n - 2j) = i + 2ni - 2i \times (i + 1) = i(2n - 2i - 1)\)。
到这就可以开始算了,拓扑序个数就是
最后算概率,总的排列个数除掉了,然后别忘了把容斥加上。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e7 + 5;
typedef long long ll;
int n, m, mod;
int inv[N], fac[N], ifac[N], ffac[N], iffac[N];
int C(int n, int m){
if(m > n) return 0;
return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
void add(ll &x, ll y){ (x += y) %= mod; }
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> m >> mod;
inv[1] = 1;
for(int i = 2; i <= 2e7; ++i) inv[i] = (1ll * (mod - mod / i) * inv[mod % i]) % mod;
fac[0] = ifac[0] = 1;
for(int i = 1; i <= 2e7; ++i) fac[i] = (1ll * fac[i - 1] * i) % mod, ifac[i] = (1ll * ifac[i - 1] * inv[i]) % mod;
ffac[0] = ffac[1] = iffac[0] = iffac[1] = 1;
for(int i = 2; i <= 2e7; ++i) ffac[i] = (1ll * ffac[i - 2] * i) % mod, iffac[i] = (1ll * iffac[i - 2] * inv[i]) % mod;
int k = n - m;
ll ans = 0;
for(int x = k; x <= n / 2; ++x){
ll p = 1ll * ((x - k) % 2 ? mod - 1 : 1) * C(x, k) % mod;
add(ans, p * C(n, 2 * x) % mod * ffac[2 * x - 1] % mod * ffac[2 * n - 3 - 2 * x] % mod * iffac[2 * n - 3] % mod);
}
cout << ans;
return 0;
}
Summary
赶紧恢复一下状态啊,这个 T1 做 1h,T2 想得太混乱了。状态不好时以部分分为主。

浙公网安备 33010602011771号