AtCoder Beginner Contest 253[题解D~Ex]
\(ABC253\)
\(D\)
\(Problem\)
找出 \(1\) 至 \(N\) 中有多少个数既不是 \(A\) 的倍数也不是 \(B\) 的倍数,求它们的和是多少。
\(1\leq N,A,B\leq 10^9\)
\(Sol\)
简单容斥。
\(A_i\) 是范围内 \(A\) 的倍数,\(B_i\) 同理,\(C_i\) 是范围内即是 \(A\) 的倍数又是 \(B\) 的倍数的数。
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e2 + 10;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, A, B, ans;
inline int gcd(int a, int b) { return !b ? a : gcd(b, a % b); }
inline int calc(int x, int y, int c) { return (x + y) * c / 2; }
signed main()
{
n = read(), A = read(), B = read();
int g = gcd(A, B);
int mi = A * B / g;
ans = calc(1, n, n);
ans = ans - calc(A, (n / A) * A, (n / A));
ans = ans - calc(B, (n / B) * B, (n / B));
ans = ans + calc(mi, (n / mi) * mi, (n / mi));
printf("%lld\n", ans);
return 0;
}
\(E\)
\(Problem\)
对于一个长为 \(N\) 的序列 \(A\),称其合法当且仅当满足一下条件:
-
\(1\leq A_i\leq M\)
-
\(|A_i - A_{i+1}|\ge K\) \((1\leq i\leq N - 1)\)
给定 \(N\),\(M\),\(K\),求合法序列的个数。
\(2\leq N\leq 1000,1\leq M\leq 5000,0\leq K\leq M - 1\)
\(Sol\)
简单 \(dp\),设 \(dp[i][j]\) 表示前 \(i\) 个数满足合法且第 \(i\) 个数为 \(j\) 的序列数量。
显然,每次更新我们要枚举当前填的数和上一个数填的数,又注意到上一个数填的数是两端连续的区间,前缀和优化可以省去枚举。
只需注意 \(K = 0\) 的情况,计算的时候两端区间可能会交在一起。
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3 + 10, M = 5e3 + 10, mod = 998244353;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m, k;
int dp[N][M], sum[M];
signed main()
{
n = read(), m = read(), k = read();
for(register int i = 1; i <= m; i++) dp[1][i] = 1;
for(register int i = 2; i <= n; i++){
//cout << "Begin: " << i << "\n";
for(register int j = 1; j <= m; j++)
sum[j] = (sum[j - 1] + dp[i - 1][j]) % mod;
for(register int j = 1; j <= m; j++){
if(j - k >= 1) dp[i][j] = (dp[i][j] + sum[j - k]) % mod;
if(j + k <= m){
int to = j + k - 1;
if(to < j - k) to = j - k;
dp[i][j] = (dp[i][j] + (sum[m] - sum[to]) % mod + mod) % mod;
}
}
}
int ans = 0;
for(register int i = 1; i <= m; i++) ans = (ans + dp[n][i]) % mod;
printf("%lld\n", ans);
return 0;
}
\(F\)
\(Problem\)
给定一个 \(N\times M\) 的矩阵,最开始矩阵里的值都为 \(0\),接下来需要完成 \(Q\) 个操作,每一个操作可能如下:
-
\(1,l,r,x\),对矩阵第 \(l\) 至 \(r\) 列的数全部加上 \(x\)。
-
\(2,i,x\),将第 \(i\) 行的元素重置为 \(x\)。
-
\(3,i,j\),输出第 \(i\) 行第 \(j\) 列的元素大小。
\(1\leq N,M,Q\leq 2\times 10 ^ 5\)
\(Sol\)
很容易想到线段树维护列,但由于行的重置操作,需要我们查找不同时间节点线段树的值。
主席树显然可以做到,只不过要区间修改,注意空间调大即可。
同样,这题也可以不用主席树维护。发现对于每一个 \(3\) 询问,我们只需要知道其对应的行与最近一次重置是什么时候,并在那个时候记录下记录下线段树对应列的值,一样可以达到目的。由于询问次数有限制,每个点在这种情况下只被记录一次,所以时间复杂度可行,且只需要一棵线段树维护。
\(code\)
主席树做法
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 10, M = 1e6 + 10;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m, Q, now;
int arr[N], brr[N];
int a[M], ls[N], rs[N], lazy[N], idx;
int root[M], sum[N];
inline void pushup(int k, int l, int r) { sum[k] = sum[ls[k]] + sum[rs[k]] + (r - l + 1) * lazy[k]; }
inline void build(int k,int l,int r)
{
lazy[k] = 0, sum[k] = 0;
if(l == r) { sum[k] = a[l]; return; }
int mid = (l + r) >> 1;
ls[k] = ++idx, rs[k] = ++idx;
build(ls[k], l, mid), build(rs[k], mid + 1, r);
pushup(k, l, r);
}
inline void update(int pre, int k, int x, int y, int val, int l, int r)
{
ls[k] = ls[pre];
rs[k] = rs[pre], sum[k] = sum[pre], lazy[k] = lazy[pre];
if(l >= x && r <= y){
lazy[k] += val;
sum[k] += (r - l + 1) * val;
return ;
}
int mid = (l + r) >> 1;
if(x <= mid) ls[k] = ++idx, update(ls[pre], ls[k], x, y, val, l, mid);
if(mid + 1 <= y) rs[k] = ++idx, update(rs[pre], rs[k], x, y, val, mid + 1, r);
pushup(k, l, r);
}
inline int query(int k, int x, int y, int lz, int l, int r)
{
if(l >= x && r <= y) return lz * (r - l + 1) + sum[k];
int mid = (l + r) >> 1;
int ans = 0;
if(x <= mid) ans += query(ls[k], x, y, lz + lazy[k], l, mid);
if(mid + 1 <= y) ans += query(rs[k], x, y, lz + lazy[k], mid + 1, r);
return ans;
}
signed main()
{
n = read(), m = read(), Q = read();
root[0]=++idx;
build(root[0],1,m);
for(register int i = 1; i <= n; i++) brr[i] = root[0];
while(Q--){
int opt = read();
if(opt == 1){
int l = read(), r = read(), x = read();
root[++now] = ++idx;
update(root[now - 1], root[now], l, r, x, 1, m);
}
if(opt == 2){
int i = read(), x = read();
arr[i] = x, brr[i] = root[now];
}
if(opt == 3){
int i = read(), j = read();
int mx = query(root[now], 1, j, 0, 1, m) - (j > 1 ? query(root[now],1,j - 1,0,1,m) : 0);
//cout << "yes\n";
int mi = query(brr[i], 1, j, 0, 1, m) - (j > 1 ? query(brr[i], 1, j - 1, 0, 1, m) : 0);
printf("%lld\n", mx - mi + arr[i]);
}
}
return 0;
}
线段树做法
#include <bits/stdc++.h>
#define int long long
#define mp make_pair
using namespace std;
const int N = 2e5 + 10;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct node{
int opt, l, r, x;
}que[N];
int n, m, Q;
int pre[N], arr[N];
map<pair<int, int>, int> vl;
int tag[4 * N];
vector<pair<int, int> > vec[N];
inline void pushdown(int k)
{
tag[k << 1] += tag[k], tag[k << 1 | 1] += tag[k], tag[k] = 0;
}
inline void change(int k, int l, int r, int x, int y, int v)
{
if(r < x || l > y) return;
if(l >= x && r <= y) { tag[k] += v; return; }
pushdown(k);
int mid = (l + r) >> 1;
change(k << 1, l, mid, x, y, v), change(k << 1 | 1, mid + 1, r, x, y, v);
}
inline int query(int k, int l, int r, int x)
{
//cout << l << " " << r << " " << x << "\n";
if(l == r && l == x) return tag[k];
pushdown(k);
int mid = (l + r) >> 1;
if(x <= mid) return query(k << 1, l, mid, x);
else return query(k << 1 | 1, mid + 1, r, x);
}
signed main()
{
n = read(), m = read(), Q = read();
for(register int i = 1; i <= Q; i++){
que[i].opt = read(), que[i].l = read(), que[i].r = read();
if(que[i].opt == 1) que[i].x = read();
if(que[i].opt == 2) pre[que[i].l] = i; //设置第 i 行新的更新位置
if(que[i].opt == 3)
if(pre[que[i].l]) vec[pre[que[i].l]].push_back(mp(que[i].l, que[i].r));
}
for(register int i = 1; i <= Q; i++){
if(que[i].opt == 1) change(1, 1, m, que[i].l, que[i].r, que[i].x);
if(que[i].opt == 2){
arr[que[i].l] = que[i].r;
for(register int j = 0; j < vec[i].size(); j++)
vl[vec[i][j]] = query(1, 1, m, vec[i][j].second);
}
if(que[i].opt == 3){
pair<int, int> x = mp(que[i].l, que[i].r);
// cout << "yes\n";
if(!vl[x]) printf("%lld\n", query(1, 1, m, que[i].r) + arr[que[i].l]);
else printf("%lld\n", query(1, 1, m, que[i].r) + arr[que[i].l] - vl[x]);
}
}
return 0;
}
\(G\)
\(Problem\)
对于任意 \(N\) 大于等于 \(2\),有 \(\frac{N(N-1)}{2}\) 个二元组 \((x,y)\) 满足 \(1\leq x < y\leq N\)。
将这些二元组以 \(x\) 为第一关键字按照字典序排列。
对于长度为 \(N\) 的序列 \(A\) 满足 \(A_i = i\),给定区间 \(L\) 和 \(R\),使之按照排名 \(L\) 至 \(R\) 的二元组一次进行如下操作:
- \(swap(A_x,A_y)\)。
输出操作后的 \(A\) 数组。
\(2\leq N\leq 2\times 10^5,1\leq L\leq R\leq \frac{N(N-1)}{2}\)
\(Sol\)
比较简单的一道找规律题。
按照 \(x\),分成不同类型的操作分别看待。
对于 \(x\) 从 \(1\) 到 \(n-1\),操作的数量从 \(n-1\) 到 \(1\)。对于每一种类型的操作,如果全部执行,效果等价于将当前的 \(A_n\) 移动到 \(A_x\),将原来的 \(A_x\) 至 \(A_{n-1}\) 向后移动一位。
显然的是,在区间内,我们只会碰到两种类型没有完全被操作,一次在开头,一次在结尾。
对于这两次操作我们找到区间暴力交换。
对于所有中间的操作,我们记录第一个被操作完的区间的位置,记录被操作完的区间的数量,则所有的操作简化为将开头的一段区间移动到末尾。
注意一些细节即可。
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, l, r;
int a[N], b[N];
int rec, cnt, flag;
signed main()
{
n = read(), l = read(), r = read();
for(register int i = 1; i <= n; i++) a[i] = i;
int sum = 0;
for(register int x = 1; x <= n; x++){ //枚举 (x,y) 中的 x
//这一轮有 n - x 种不同的数
sum = sum + (n - x);
if(sum >= l && !flag){ //刚刚进入
if(sum <= r){
flag = 1, rec = x + 1;
int mi = n - (sum - l);
for(register int y = mi; y <= n; y++) swap(a[x], a[y]);
}
else{
int mi = n - (sum - l), mx = n - (sum - r);
for(register int y = mi; y <= mx; y++) swap(a[x], a[y]);
break;
}
}
else if(flag == 1 && sum < r) cnt++; //吃掉了一轮的数
else{
if(flag == 1 && sum >= r){
for(register int i = 1; i <= n; i++) b[i] = a[i];
for(register int i = rec + cnt; i <= n; i++) a[i] = b[i - cnt];
for(register int i = rec, j = n; i < rec + cnt; i++, j--) a[i] = b[j];
int mx = n - (sum - r);
for(register int y = x + 1; y <= mx; y++) swap(a[x], a[y]);
flag = 2;
}
}
}
for(register int i = 1; i <= n; i++) printf("%lld ", a[i]);
return 0;
}
\(Ex\)
\(Problem\)
给定 \(N\) 个点,以及两个长为 \(M\) 的序列 \(u\) 和 \(v\)。
执行以下操作 \(N-1\) 次:
- 等概率的选择一个 \(i(1\leq i\leq M)\),连接点 \(u_i\) 和 \(v_i\)。
问 \(k\) 次操作后有多少概率这张无向图会是一个森林,并对 \(998244353\) 取模。
\(2\leq N\leq 14,N-1\leq M\leq 500,1\leq u_i,v_i\leq N,u_i\neq v_i\)
\(Sol\)
官方题解用到了矩阵树定理,但是我不会。有一个用户题解给出了不使用矩阵树定理的做法。
设集合 \(V=\{1,2,3……N \}\)
设 \(f_S\) 表示 \(S\subseteq V\) ,且以集合 \(S\) 中的点形成一棵树的方案数。
设 \(g_{S,i}\) 表示 \(S\subseteq V\),且以集合 \(S\) 中的点形成一片森林且边的数量为 \(i\) 的方案数。
那么,对于 \(k=i\),答案即是 $$\frac{g_{V,i}\times i!}{m^i}$$
我们只需考虑如何求解 \(g\) 数组。
首先考虑求解 \(f\) 数组。
事实上,\(f\) 数组满足一下递推关系:
-
\[f_S = 1,|S| \leq 1 \]
-
\[f_S = \frac{1}{2\times(|S|-1)}\sum_{T\subset S}f_T\times f_{S/T}\times val(T,S/T) \]
其中 \(S/T\) 表示 \(T\) 在 \(S\) 范围下的补集。\(val(U,V)\) 表示点集 \(U\) 和点集 \(V\) 之间边的数量,\(val(U,V)\) 求解如下:
-
设 \(e_S\) 表示 \(u_i\) 和 \(v_i\) 均在点集 \(S\) 中的边的数量。
-
\(val(U,V) = e(U\cup V)-e(U)-e(V)\)
这是个简单的容斥,不难理解。
简单分析一下 \(f\) 数组求解原理,即相当于两颗树通过连边合并到一起。之所以除以 \(2\times (|S| - 1)\),\(2\) 是因为我们通过计算补集递推 \(f_S\),则相同的状态我们会计算两次,\(|S|-1\) 是因为一个节点数为 \(|S|\) 的树有 \(|S|-1\) 条边,合并时我们分为两个部分合并,一个形态相同的树可以按照 \(|S|-1\) 个边断开得到 \(|S| -1\) 个不同的组合。
求得 \(f\) 数组后,即可递推 \(g\):
这是个合并的过程,相当于将一个 \(|T|\) 个节点的树合并到了一个森林中。
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1 << 14, M = 5e2 + 10, mod = 998244353;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m;
int fac[N], siz[N];
int f[N], e[N], g[N][M]; //e[i] 表示子集 i 中共有多少种可能的边
inline int val(int x, int y) { return e[x | y] - e[x] - e[y]; } //求子集 x 和 y 中分别有多少边
inline int power(int x, int k)
{
int res = 1;
while(k){
if(k & 1) res = res * x % mod;
x = x * x % mod, k >>= 1;
}
return res;
}
vector<int> vec[20];
signed main()
{
n = read(), m = read(), fac[0] = 1;
for(register int i = 1; i < (1 << n); i++) siz[i] = siz[i >> 1] + (i & 1);
for(register int i = 1; i < n; i++) fac[i] = fac[i - 1] * i % mod;
for(register int i = 1; i <= m; i++){
int x = read(), y = read();
for(register int j = 0; j < N; j++){ //枚举子集
if(!(j & (1 << (x - 1))) || !(j & (1 << (y - 1)))) continue;
e[j]++;
}
}
for(register int i = 1; i < (1 << n); i++) vec[siz[i]].push_back(i);
f[0] = 1;
for(register int i = 0; i < vec[1].size(); i++) f[vec[1][i]] = 1; //集合大小为 1
for(register int t = 2; t <= n; t++){ //枚举集合大小
for(register int i = 0; i < vec[t].size(); i++){ //枚举集合
int res = 0, s = vec[t][i];
for(register int a = (s - 1) & s; a ; a = (a - 1) & s){ //枚举 vec[t][i] 下的子集
int b = s ^ a;
res = (res + f[a] * f[b] % mod * val(a, b) % mod) % mod;
}
f[s] = res * power(2 * (t - 1), mod - 2) % mod;
}
}
// for(register int i = 0; i < (1 << n); i++) cout << f[i] << "\n";
for(register int i = 0; i < n; i++){
for(register int s = 0; s < (1 << n); s++){
if(i == siz[s] - 1) g[s][i] = f[s];
int p = s & (-s);
for(register int a = s; a; a = (a - 1) & s){
if((a & p) == 0) continue;
if(i - siz[a] + 1 >= 0)
g[s][i] = ((f[a] * g[s ^ a][i - siz[a] + 1]) % mod + g[s][i]) % mod;
}
//cout << i << " " << s << " " << g[s][i] << endl;
}
}
int v = (1 << n) - 1;
for(register int i = 1; i < n; i++){
printf("%lld\n", g[v][i] * fac[i] % mod * power(power(m, i), mod - 2) % mod);;
}
return 0;
}