2024 1 月杂题选讲
2024 1 月杂题选讲
[集训队互测 2023] 基础寄术练习题
的情况,如果已经确定了这个序列 ,则该式意义为,将总共 种颜色的球,第 种颜色是 ,有 个,这 个球排成一排,第一个球是 种颜色,第二种出现的颜色的球是第 种颜色,第 种球是 种颜色的概率,再除以 。在 的时候,所有排列的答案之和为 ,在 的时候,枚举 是什么,然后计算满足上面条件的排列方案数。考虑容斥,枚举一个集合在 后面出现,直接 dp 即可。
signed main()
{
cin >> n >> m >> k >> mod;
inv[1] = 1;
for (rint i = 2; i <= N; i++)
{
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
if (k == 1)
{
g[0] = 1;
for (rint i = 1; i <= m; i++)
{
for (rint j = i - 1; ~j; j--)
{
g[j + 1] += inv[i] * g[j] % mod;
g[j + 1] %= mod;
}
}
cout << g[n] << endl;
return 0;
}
f[0][0][0][0] = 1;
for (rint i = 1, p = 1, q = 0; i <= m; s += i, i++, p ^= 1, q ^= 1)
{
for (rint j = 0; j < i; j++)
{
for (rint k = 0; k <= s; k++)
{
for (rint l = 0; l <= 1; l++)
{
int w = f[q][j][k][l];
f[q][j][k][l] = 0;
f[p][j][k][l] += w;
f[p][j][k][l] %= mod;
f[p][j + 1][k][l] += w * inv[i] % mod;
f[p][j + 1][k][l] %= mod;
f[p][j + 1][k + i][l] += (mod - w) * inv[i] % mod;
f[p][j + 1][k + i][l] %= mod;
if (!l)
{
f[p][j + 1][k + i][1] += w * i % mod;
f[p][j + 1][k + i][1] %= mod;
}
}
}
}
}
for (rint i = 0; i <= s; i++)
{
ans += f[m & 1][n][i][1] * inv[i] % mod;
ans %= mod;
}
cout << ans << endl;
return 0;
}
[国家集训队] Crash 的文明世界
考虑树形 dp 的 up and down trick,求这个柿子
考虑树形 ,在第一遍 的时候,我们设 为
表示 在 子树内部
二项式定理展开
的方程式:
的时候 表示是整棵树,到达 首先要到达 ,于是就有
考虑继续优化,发现求 可以理解为把 个物品放到 个互不相同的盒子里,允许有盒子空着不放的方案数,于是写成 其中 是第二类斯特林数,表示的是将 个球分到 个盒子里,这 个盒子没有差别,而且没有盒子是空的的方案数。
继续化柿子
原问题转化为求出
再用类似于上方想法的树形 dp 转移一下就可以了。
void dfs1(int x)
{
f[x][0] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!d[y])
{
d[y] = d[x] + 1;
dfs1(y);
f[x][0] += f[y][0];
for (rint j = 1; j <= m; j++)
{
f[x][j] = (f[x][j] + f[y][j - 1] + f[y][j]) % mod;
}
}
}
}
void dfs2(int x)
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y] > d[x])
{
memset(a, 0, sizeof a);
a[0] = f[x][0] - f[y][0];
for (rint j = 1; j <= m; j++)
{
a[j] = (f[x][j] - f[y][j] - f[y][j - 1] + mod) % mod;
}
f[y][0] += a[0];
for (rint j = 1; j <= m; j++)
{
f[y][j] = (f[y][j] + a[j] + a[j - 1]) % mod;
}
dfs2(y);
}
}
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
s[0][0] = 1;
for(rint i = 1; i <= m; i++)
{
s[i][1] = 1;
s[i][i] = 1;
}
for (rint i = 1; i <= m; i++)
{
for (rint j = 1; j < i; j++)
{
s[i][j] = (s[i - 1][j - 1] + s[i - 1][j] * j) % mod;
}
}
fac[0] = 1;
for (rint i = 1; i <= m; i++)
{
fac[i] = (fac[i - 1] * i) % mod;
}
d[1] = 1;
dfs1(1);
dfs2(1);
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= m; j++)
{
f[i][j] = (f[i][j] % mod + mod) % mod;
}
}
for (rint i = 1; i <= n; i++)
{
int ans = 0;
for (rint j = 1; j <= m; j++)
{
ans = (ans + s[m][j] * fac[j] % mod * f[i][j]) % mod;
}
cout << ans << endl;
}
return 0;
}
[CTSC2007] 挂缀
很简单的一个反悔贪心,考虑何时为最优序列,显然有
后面的操作将已选的珠子放入一个优先队列,如果当前珠子不够加入,还比队首轻就那就将队首珠子删去。
实现的话怎么实现都行。
[WC2021] 表达式求值
做的时候人是麻的,就知道要中缀转后缀,然后就不知道怎么写了,后来学习了一下 wrpwrp 大佬的题解。
首先每一位是独立的, 互相之间不影响, 所以可以把每一位拆开考虑。
发现每次你只要考虑 个数, 但是 复杂度依赖于操作串的长度, 考虑优化,发现所有操作都是最小最大, 只和大小有关而和具体值是多少关系不大。
枚举一个数当做答案, 然后大于等于这个数的设为 , 小于的设为 。然后 一遍求出最后结果为 的方案数, 就是最后的结果大于等于当前枚举的值的结果。简单差分即可得到答案。同时由于转化成为了 序列, 就只有 种情况了, 预处理一下就好了。
int built(int l, int r)
{
if (l == r)
{
++idx;
val[idx] = s[l] - '0';
return idx;
}
if (p[r] == l) return built(l + 1, r - 1);
int id = ++idx, pos = p[r] - 1;
son[0][id] = built(l, pos - 1), son[1][id] = built(pos + 1, r);
if (s[pos] == '?') val[id] = 10;
else if (s[pos] == '<') val[id] = 11;
else val[id] = 12;
return id;
}
void dp(int x)
{
if (val[x] < 10)
{
f[x][(k >> val[x]) & 1] = 1;
return ;
}
dp(son[0][x]);
dp(son[1][x]);
for (rint i = 0; i <= 1; i++)
{
if (f[son[0][x]][i])
{
for (rint j = 0; j < 2; j++)
{
if (f[son[1][x]][j])
{
int tmp = f[son[0][x]][i] * f[son[1][x]][j] % M;
if (val[x] == 10)
{
(f[x][i] += tmp) %= M;
(f[x][j] += tmp) %= M;
}
if (val[x] == 11)
{
(f[x][min((int)i, (int)j)] += tmp) %= M;
}
if (val[x] == 12)
{
(f[x][max((int)i, (int)j)] += tmp) %= M;
}
}
}
}
}
}
signed main()
{
cin >> n >> m;
for (rint i = 0; i < m; i++)
{
for (rint j = 1; j <= n; j++)
{
cin >> a[i][j];
}
}
scanf("%s", s + 1);
int leng = strlen(s + 1);
for (rint i = 1; i <= leng; i++)
{
if (s[i] == '(') stk.push((int)i);
if (s[i] == ')') p[i] = stk.top(), stk.pop();
if ('0' <= s[i] && s[i] <= '9') p[i] = i;
}
root = built(1, leng);
for (k = 0; k < (1 << m); k++)
{
memset(f, 0, sizeof(f));
dp(root);
g[k] = f[root][1];
}
for (rint i = 1; i <= n; i++)
{
vector<pair<int, int> > v;
for (rint j = 0; j < m; j++) v.push_back({a[j][i], (int)j});
sort(v.begin(), v.end());
int now = (1 << m) - 1, j = 0;
(ans += v[0].x * g[now] % M) %= M;
while(j < m)
{
int x = v[j].x;
while (j < m && v[j].x == x)
{
now ^= (1 << v[j].y);
j++;
}
if (now > 0)
{
(ans += g[now] * (v[j].x - x) % M) %= M;
}
}
}
cout << ans << endl;
return 0;
}
[CTSC2016] 单调上升序列
很有意思的一个构造题。
把边分成 个小组,对于同个中的边赋连续区间的值,限制最长单调上升序列不超过小组的数量。
现在,讲原问题转变为 :给出一张 个点的完全图,把边分为 组完美匹配
对于所有 ,我们把 这条边归入编号为 的集合。处理所有连向 的边; 发现对于每个集合编号 ,有且仅有一个 使得 ,把 在当前集合里连向 即可。
signed main()
{
cin >> n;
for (rint i = 0; i <= n - 2; i++)
{
a[i] = i * n / 2;
}
for (rint i = 0; i <= n - 2; i++)
{
int k;
for (rint j = i + 1; j <= n - 2; j++)
{
k = (i + j) % (n - 1);
a[k]++;
cout << a[k] << " ";
}
k = i * 2 % (n - 1);
a[k]++;
cout << a[k] << endl;
}
return 0;
}
[CTS2023] 琪露诺的符卡交换
将题意转化为如何钦定每一排的顺序,使得每种颜色在每一列都出现过。
将每一排和它有的颜色连边,建出二分图,对于每一列跑一个二分图最大匹配,得出每一个颜色的位置。
对于两侧点集为 的二分图, 表示点集 的邻域,若 ,那么存在完美匹配。
考虑在本题中,经过 轮删边后,每个点的度数为 ,对于左侧任意一个大小为 的点集,向右侧连边数为 。若邻域小于 ,那么一定存在一个点的度数大于 ,不符合题意,所以一定有完美匹配。
bool dfs(int x)
{
if (v[x]) return 0;
v[x] = 1;
for (rint y = 1; y <= n; y++)
{
if (s[x][y] != 0 && (!fa[y] || dfs(fa[y])))
{
fa[y] = x; return 1;
}
}
return 0;
}
signed main()
{
int T;
cin >> T;
while (T--)
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
cin >> a[i][j];
s[i][a[i][j]]++;
}
}
for (rint j = 1; j <= n; j++)
{
memset(fa, 0, n + 1 << 2);
for (rint i = 1; i <= n; i++)
{
memset(v, 0, n + 1);
dfs(i);
}
for (rint i = 1; i <= n; i++)
{
int k = 1;
while (a[fa[i]][k] != i) k++;
a[fa[i]][k] = 0;
b[fa[i]][j] = k;
s[fa[i]][i]--;
}
}
int ans = n * (n - 1) >> 1;
cout << ans << endl;
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j < i; j++)
{
printf("%d %d %d %d\n", i, b[i][j], j, b[j][i]);
}
}
}
return 0;
}
[清华集训2017] 榕树之心
设一个 为从以 为根的子树开始移动最终能离 最近的距离
记为每个结点的重儿子
如果 ,从重儿子中移动,离重儿子最近的距离 ,即为离当前结点的距离,如果此距离小于其它可以移动的结点数量,则当 时,则 ,否则即
反之,则
考虑优化。遍历时维护一个 缩点后的重儿子 ,
当 时,用次重儿子进行判断。维护一个次重儿子,每个点的重儿子和次重儿子和 一样预处理好,当我们遍历向下一个结点拓展时讨论情况更新即可。
int cmp(int x, int y)
{
return size[x] > size[y] ? x : y;
}
void dfs1(int x, int father)
{
size[x] = 1;
d[x] = d[father] + 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (y == father)
{
continue;
}
dfs1(y, x);
size[x] += size[y];
if (size[y] > size[son[x][1]])
{
son[x][2] = son[x][1];
son[x][1] = y;
}
else if (size[y] > size[son[x][2]])
{
son[x][2] = y;
}
}
if (size[son[x][1]] - 2 * f[son[x][1]] <= size[x] - size[son[x][1]] - 1)
{
f[x] = (size[x] - 1) / 2;
}
else
{
f[x] = size[x] - 1 - size[son[x][1]] + f[son[x][1]];
}
}
void dfs2(int x, int father, int t)
{
int k = cmp(t, son[x][1]);
if (size[k] - 2 * f[k] <= n - d[x] - size[k])
{
ans[x] = (n & 1) == (d[x] & 1);
}
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (y == father)
{
continue;
}
dfs2(y, x, y == son[x][1] ? cmp(son[x][2], t) : cmp(son[x][1], t));
}
}
signed main()
{
cin >> w >> T;
while (T--)
{
idx = 0;m(f);m(h);m(e);m(ne);m(son);m(size);m(ans);
cin >> n;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
dfs1(1, 0);
dfs2(1, 0, 0);
if (w == 3)
{
cout << ans[1] << endl;
}
else
{
for (rint i = 1; i <= n; i++)
{
cout << ans[i];
}
cout << endl;
}
}
return 0;
}
[清华集训2016] 如何优雅地求和
学习了一手 Binomial Sum 的微积分做法,很新颖。
。
然后求
构造 。
,设 ,所以 。
变为
考察 其不合法的项,只有 从其原函数的 次跨向 次中来
所以 ,具体地 应该获得贡献。但是实际上不存在。所以要在右边减去。
右边等于
发现右边是一个可以变化、展开的项,令
然后得到
设 , 左式
同时 右式 。
由于两边多项式相等,系数也相等,左式等于右式,得出 递推式。
到 ,倒着推更优。
卡老师还有一个神奇做法,见 link
int qpow(int a, int b)
{
int s = 1;
while (b)
{
if(b & 1) s = s * a % mod;
a = a * a % mod, b >>= 1;
}
return s % mod;
}
int power(int x){return x % 2 ? mod - 1 : 1;}
int subm(int x){return (x % mod + mod) % mod;}
int C(int a, int b){return fac[a] * ifac[b] % mod * ifac[a - b] % mod;}
void init()
{
fac[0] = 1;
for (rint i = 1; i <= m; i++)
{
fac[i] = fac[i - 1] * i % mod;
}
ifac[m] = qpow(fac[m], mod - 2);
for (rint i = m - 1; i >= 0; i--)
{
ifac[i] = ifac[i + 1] * (i + 1) % mod;
}
}
signed main()
{
cin >> n >> m >> a;
for (rint i = 0; i <= m; i++)
{
cin >> g[i];
}
init();
int res = 0;
if (n <= m)
{
for (rint i = 0; i <= n; i++)
{
res += g[i] * C(n, i) % mod * qpow(a, i) % mod * qpow(mod + 1 - a, n - i) % mod;
res %= mod;
}
cout << res << endl;
return 0;
}
int k = ifac[m];
int p = qpow(a, m + 1);
for (rint i = 1; i <= m; i++)
{
k = k * (n - i) % mod;
}
f[m + 1] = 0;
for (rint i = m; i >= 0; i--)
{
f[i] = qpow(a * (n - i) % mod, mod - 2) * subm((i + 1) * (mod + 1 - a) % mod * f[i + 1] % mod - n * p % mod * k % mod * C(m, i) % mod * power(m - i + 1) % mod) % mod;
}
for (rint i = 0; i <= m; i++)
{
res += g[i] * f[i] % mod;
res %= mod;
}
cout << res << endl;
return 0;
}
[清华集训2016] 组合数问题
卢卡斯的过程,实际上就是把最后一位数单独拿出来算,然后把最后一位去掉。把两个位置上的数都拆分的话,那么就相当于每一个位置上的组合数乘起来。
原问题变为,有多少个 的数对 ,满足在 进制拆分下的某一位数
数位 dp 设 表示当前在 位,是否满足要求, 的填法是否危险, 的填法是否危险。记忆化转移即可。
int dfs(int pos, bool c, bool flag, bool t1, bool t2)
{
if (!pos) return c;
auto &res = f[pos][c][flag][t1][t2];
if (res != -1) return res;
res = 0;
int d = t1 ? a[pos] : k - 1, q = t2 ? b[pos] : k - 1;
for (rint i = 0; i <= d; i++)
{
for (rint j = 0; (j <= i || flag) && j <= q; j++)
{
bool a = c, b = flag;
a |= (i < j), b |= (i != j);
(res += dfs(pos - 1, a, b, t1 && i == d, t2 && j == q)) %= mod;
}
}
return res;
}
int solve(int n, int m)
{
int len = 0, lem = 0;
memset(a, 0, sizeof a);
memset(b, 0, sizeof b);
memset(f, -1, sizeof f);
while (n) a[++len] = n % k, n /= k;
while (m) b[++lem] = m % k, m /= k;
return dfs(max(len, lem), 0, 0, 1, 1);
}
signed main()
{
int T;
cin >> T >> k;
while (T--)
{
int a, b;
cin >> a >> b;
cout << solve(a, b) << endl;
}
return 0;
}
[清华集训2015] 灯泡
简单数学题,也可以三分,做法很多,而且实际上不卡
如果墙上有影子,那么
如果 ,答案为 ;
如果 ,答案为 ;
否则答案为
[清华集训 2017] 福若格斯
可以通过这个题学习不平等博弈,由于整理时间较长,直接给自己留个链接算了,以后回来看看 Arahc's blog
int qpow(int a, int b)
{
int s = 1;
while (b)
{
if(b & 1) s = s * a % mod;
a = a * a % mod, b >>= 1;
}
return s % mod;
}
int C(int x, int y)
{
if (y < 0 || x < y) return 0;
return fac[x] * ifac[y] % mod * ifac[x - y] % mod;
}
int calc(int x, int y, int z) {return C(x + y, y + z);}
void solve()
{
int star = 0, up = 0, dwn = 0, c0 = 0, c1 = 0, f1 = 0, c2 = 0, f2 = 0, n;
cin >> n;
for (rint i = 1; i <= n; i++)
{
int x;
string s;
cin >> s >> x;
if (s == "LL_RR" || s == "LRL_R" || s == "L_RLR") star += x;
else if (s == "LRRL_" || s == "RLRL_" || s == "RRL_L") c1 += x;
else if (s == "_RLRL" || s == "_RLLR" || s == "R_RLL") f1 += x;
else if (s == "RL_LR") c2 += x;
else if (s == "LR_RL") f2 += x;
else if (s == "L_LRR") up += x;
else if (s == "LLR_R") dwn += x;
else c0 += x;
}
int wL = 0, wR = 0, wfir = 0, wsec = 0, w0 = 0;
int all = qpow(2, c1 + f1);
int sumL = 0, sumR = 0;
for (rint i = -f1; i <= c1; i++)
{
pre[i + f1] = (i == -f1 ? 0 + calc(c1, f1, i) : pre[i - 1 + f1] + calc(c1, f1, i)) % mod;
}
for (rint i = c1; i >= -f1; i--)
{
suf[i + f1] = (i == c1 ? 0 + calc(c1, f1, i) : suf[i + 1 + f1] + calc(c1, f1, i)) % mod;
}
for (rint i = -f2; i <= c2; i++)
{
int val = calc(c2, f2, i), c0 = 0;
int pL = -i / 2, pR = -i / 2;
while (pL * 2 + i <= 0) pL++;
while ((pL - 1) * 2 + i > 0) pL--;
while (pR * 2 + i >= 0) pR--;
while ((pR + 1) * 2 + i < 0) pR++;
sumL = pL > c1 ? 0 : suf[max(0ll, pL + f1)];
sumR = pR < -f1 ? 0 : pre[min(c1 + f1, pR + f1)];
if (i % 2 == 0) c0 = calc(c1, f1, -i / 2);
if (!val) continue;
wL = (wL + val * (sumL + mod)) % mod;
wR = (wR + val * (sumR + mod)) % mod;
w0 = (w0 + val * c0) % mod;
}
int sstar = (star == 0 ? 1 : qpow(2, star - 1));
int suf = 0, pre = 0;
for (rint i = up; i > 1; i--)
{
suf = (suf + calc(up, dwn, i)) % mod;
}
for (rint i = -dwn; i < -1; i++)
{
pre = (pre + calc(up, dwn, i)) % mod;
}
int g1 = calc(up, dwn, 1);
int g_1 = calc(up, dwn, -1);
int g0 = calc(up, dwn, 0);
all = qpow(2, up + dwn + star);
wL = (wL * all % mod + sstar * w0 % mod * (g1 + suf)) % mod;
wR = (wR * all % mod + sstar * w0 % mod * (g_1 + pre)) % mod;
wsec = (wsec + w0 * sstar % mod * g0) % mod;
sstar = (star == 0 ? 0 : qpow(2, star - 1));
wL = (wL + sstar * w0 % mod * suf) % mod;
wR = (wR + sstar * w0 % mod * pre) % mod;
wfir = (wfir + (g0 + g1 + g_1) % mod * sstar % mod * w0) % mod;
wL = wL * qpow(2, c0) % mod;
wR = wR * qpow(2, c0) % mod;
wfir = wfir * qpow(2, c0) % mod;
wsec = wsec * qpow(2, c0) % mod;
wL = (wL + mod) % mod;
wR = (wR + mod) % mod;
wfir = (wfir + mod) % mod;
wsec = (wsec + mod) % mod;
cout << wL << " " << wR << " " << wfir << " " << wsec << endl;
}
signed main()
{
fac[0] = ifac[0] = 1;
for (rint i = 1; i <= N; i++)
{
fac[i] = fac[i - 1] * i % mod;
}
ifac[N] = qpow(fac[N], mod - 2);
for (rint i = N - 1; i >= 1; i--)
{
ifac[i] = ifac[i + 1] * (i + 1) % mod;
}
cin >> num >> T;
while (T--)
{
solve();
}
return 0;
}
CF1801E Gasoline prices
表示从点 向祖先方向的前 个点构成的链, 表示从点 向祖先方向的前 个点构成的链翻转后的结果
第 层的并查集中包含 和 中的所有元素,从 到 编号。对于一个限制,求两条链上的点的值一一对应相等,在并查集中直接合并,以两条链的 为分界分成 部分,其中一部分是把一个 和一个 合并,还有两部分是把两个 合并。并查集中的每一层最多被合并 次,所以总复杂度仍然是。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 5;
const int M = 4e5 + 5;
const int mod = 1e9 + 7;
int n, q, fa[N][25], l[N], r[N];
int dep[N];
int ans = 1;
int h[N], e[N], ne[N], idx;
int qpow(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void dfs(int x, int depth)
{
dep[x] = depth;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
dfs(y, depth + 1);
}
}
int lca(int x, int y)
{
for (rint i = 18; i >= 0; i--)
if (fa[x][i] > 0 && dep[fa[x][i]] >= dep[y])
x = fa[x][i];
for (rint i = 18; i >= 0; i--)
if (fa[y][i] > 0 && dep[fa[y][i]] >= dep[x])
y = fa[y][i];
if (x == y) return x;
for (rint i = 18; i >= 0; i--)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
int get(int x, int y)
{
for (rint i = 0; i <= 18; i++) if (y & (1 << i)) x = fa[x][i];
return x;
}
struct dsu
{
int fa[M];
dsu()
{
for (rint i = 0; i < M; i++) fa[i] = i;
}
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
bool merge(int x, int y, int p)
{
if (find(x) == find(y)) return 0;
if (!p)
{
ans *= qpow((r[fa[x]] - l[fa[x]] + 1) * (r[fa[y]] - l[fa[y]] + 1) % mod, mod - 2);
ans %= mod;
ans *= max(min(r[fa[x]], r[fa[y]]) - max(l[fa[x]], l[fa[y]]) + 1, 0ll);
ans %= mod;
l[fa[y]] = max(l[fa[y]], l[fa[x]]);
r[fa[y]] = min(r[fa[y]], r[fa[x]]);
}
fa[find(x)] = find(y);
return 1;
}
} s[25];
void merge(int x, int y, int p)
{
if (!s[p].merge(x, y, p) || !p)return;
merge(x, y, p - 1);
merge(fa[x][p - 1], fa[y][p - 1], p - 1);
}
void merge_path(int x, int y, int p)
{
int v = __lg(p);
merge(x, y, v);
merge(get(x, p - (1 << v)), get(y, p - (1 << v)), v);
}
void mergeR(int x, int y, int p)
{
if (!s[p].merge(x, y + (p == 0 ? 0 : n), p) || !p) return;
mergeR(x, fa[y][p - 1], p - 1);
mergeR(fa[x][p - 1], y, p - 1);
}
void merge_pathR(int x, int y, int p)
{
int v = __lg(p);
mergeR(x, get(y, p - (1 << v)), v);
mergeR(get(x, p - (1 << v)), y, v);
}
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
signed main()
{
cin.tie(0);
cout.tie(0);
cin >> n;
for (rint i = 2; i <= n; i++)
{
fa[i][0] = read();
add(fa[i][0], i);
}
for (rint i = 0; i < 19; i++)
{
for (rint j = 1; j <= n; j++)
{
fa[j][i + 1] = fa[fa[j][i]][i];
}
}
for (rint i = 1; i <= n; i++)
{
l[i] = read();
r[i] = read();
ans *= (r[i] - l[i] + 1);
ans %= mod;
}
dfs(1, 0);
cin >> q;
while (q--)
{
int a = read(), b = read(), c = read(), d = read();
if (!ans)
{
cout << 0 << endl;
continue;
}
int p = lca(a, b);
int q = lca(c, d);
if (dep[a] - dep[p] < dep[c] - dep[q])
{
swap(a, c);
swap(b, d);
swap(p, q);
}
int l1 = dep[c] - dep[q] + 1;
int l2 = (dep[a] - dep[p]) - (dep[c] - dep[q]);
int l3 = dep[a] + dep[b] - dep[p] * 2 + 1 - l1 - l2;
if (l1) merge_path(a, c, l1);
if (l2) merge_pathR(get(a, l1), get(d, l3), l2);
if (l3) merge_path(b, d, l3);
cout << ans << endl;
}
return 0;
}
[集训队互测 2023] 优惠购物
先想象这个题有没有一些性质,如果一次性使用了 个金币,那么在更前面使用更优。对于每个数最上面的 个金币,优惠券越前面用越好。思路大概有了,应该是个思维题,纯贪心有可能莽过去。
从前往后考虑每个物品,并优先使用优惠券。反悔贪心,假设对 使用 个优惠券,最上面 个优惠券不会被用金币反悔,因为一个金币换一个优惠券不优。中间有若干段整段的 个优惠券,如果用 个金币反悔,会得到 个优惠券。最后有一个非整段的优惠券,同样的,如果用金币去完全兑换,可以多得到一张优惠券,否则只能得到金币对应数量的优惠券。
对于一个物品,如果有可以反悔的一段优惠券,肯定优先使用金币去反悔这段优惠券,而不是直接用金币去购买这段商品。考虑需要的优惠券不足以兑换一整段之前的优惠券的情况,这时需要考虑到底是将前面的一段优惠券兑换一部分,还是将金币留给自己,这个可以根据留给自己是否存在一张多余优惠券,以及两者的大小关系来判断。用堆实现,时间复杂度 。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
#define x first
#define y second
using namespace std;
const int N = 1e6 + 5;
int n, m, c;
int a[N], b[N];
signed main()
{
cin.tie(0);
cout.tie(0);
int T;
cin >> T;
while (T--)
{
cin >> n >> m >> c;
for (rint i = 1; i <= n; i++) cin >> a[i];
for (rint i = 1; i <= n; i++) cin >> b[i];
priority_queue<pair<int, int> > q;
int ans = 0;
for (rint i = 1; i <= n; i++)
{
while (!q.empty() && b[i] > m)
{
auto p = q.top();
q.pop();
p.x *= -1;
int w = (b[i] - m + 1) / (p.x + 1);
if (!w)
{
if (c - (a[i] - b[i]) % c < p.x && a[i] / c != (a[i] - b[i]) / c)
{
q.emplace(-p.x, p.y);
if (c - (a[i] - b[i]) % c <= b[i] - m)
{
int w = c - (a[i] - b[i]) % c;
b[i] -= w;
continue;
}
break;
}
ans += b[i] - m;
q.emplace(-(p.x - (b[i] - m)), 1);
p.y--;
if (p.y) q.emplace(-p.x, p.y);
m = b[i];
break;
}
if (w >= p.y)
{
ans += p.x * p.y;
m += (p.x + 1) * p.y;
continue;
}
ans += w * p.x;
m += w * (p.x + 1);
if (w ^ p.y) q.emplace(-p.x, p.y - w);
}
int w = min(b[i], m);
if (w <= a[i] % c)
{
ans += a[i] - w;
m -= w;
m += (a[i] - w) / c;
}
else
{
ans += a[i] - w;
m -= w;
m += (a[i] - w) / c;
w -= a[i] % c;
if (w % c) q.emplace(-(w % c), 1);
if (w / c) q.emplace(-c, w / c);
}
}
cout << ans << endl;
}
return 0;
}
[集训队互测 2023]Permutation Counting 2
钦定两维分别有 个 <
,最后的答案可以二项式反演得出。有 个 <
等价于有 个连续段,转化成钦定有 个连续段。
考虑排列与其逆排列连续段之间的关系,发现当在放原排列的一个连续段的时候,其会在逆排列的若干个连续段最后加上若干个数。设 表示在放原排列第 个连续段的时候在逆排列第 个连续段最后放了 个数,则满足如下条件的 唯一对应一个排列:
- 。
- 每行每列都有值。
前一个限制可以插板法做,后一个限制可以钦定某几行某几列为空,然后容斥,同样可以拆开成两维分别来一遍,时间复杂度
int n, m, k, p;
int ans[N][N], c[N][N];
int fac[M], ifac[M];
int f[N][N];
int qpow(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1) res = res * a % p;
b >>= 1;
a = a * a % p;
}
return res;
}
void init()
{
fac[0] = ifac[0] = 1;
for (rint i = 1; i < M; i ++)
{
fac[i] = fac[i - 1] * i % p;
ifac[i] = ifac[i - 1] * qpow(i, p - 2) % p;
}
}
int C(int a, int b)
{
if (b < 0 || a < b) return 0;
return fac[a] * ifac[a - b] % p * ifac[b] % p;
}
signed main()
{
cin >> n >> p;
init();
for (rint i = 0; i <= n; i++)
{
c[i][0] = 1;
for (rint j = 1; j <= i; j++)
{
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % p;
}
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
f[i][j] = C(i * j + n - 1, i * j - 1);
}
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
for (rint k = 1; k < i; k++)
{
f[i][j] = (f[i][j] + (p - f[k][j]) * c[i][k]) % p;
}
}
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
for (rint k = 1; k < j; k++)
{
f[i][j] = (f[i][j] + (p - f[i][k]) * c[j][k]) % p;
}
}
}
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= n; j++)
{
ans[n - i][n - j] = f[i][j];
}
}
for (rint i = 0; i < n; i++)
{
for (rint j = n - 1; ~j; j--)
{
for (rint k = j + 1; k < n; k++)
{
ans[i][j] = (ans[i][j] + (p - ans[i][k]) * c[k][j]) % p;
}
}
}
for (rint i = n - 1; ~i; i--)
{
for (rint j = 0; j < n; j++)
{
for (rint k = i + 1; k < n; k++)
{
ans[i][j] = (ans[i][j] + (p - ans[k][j]) * c[k][i]) % p;
}
}
}
for (rint i = 0; i < n; i++)
{
for (rint j = 0; j < n; j++)
{
printf("%lld%c", ans[i][j], " \n"[j == n - 1]);
}
}
return 0;
}
[THUPC 2024 初赛] 排序大师
考虑在排列开头加上 ,结尾加上 ,然后建边 ,最终状态是 个环。
假设我们现在操作 ,那么原来的边是 ,,,。 现在的边是 。 这样的操作的任意性不会强于两次选择两个点,交换其出边。选择两个点交换出边只会增加至多 个环,所以,如果能每次减少两个环,那么这个交换次数就是顶到下界的。
构造方法:
- 选择最小的点 ,满足 前面存在一个 ,选择这个
- 容易说明, 在 前面, 在 后面,因此将 换到 前面, 换到 后面即可增加两个自环。
时间复杂度 。
int n, f[N], w[N];
vector<tuple<int, int, int, int> > ans;
void solve(int a, int b, int c, int d)
{
memset(w, 0, sizeof w);
int idx = -1;
for (rint i = 0; i < a; i++) w[++idx] = f[i];
for (rint i = c; i <= d; i++) w[++idx] = f[i];
for (rint i = b + 1; i < c; i++) w[++idx] = f[i];
for (rint i = a; i <= b; i++) w[++idx] = f[i];
for (rint i = d + 1; i <= n + 1; i++) w[++idx] = f[i];
memcpy(f, w, sizeof f);
ans.emplace_back(a, b, c, d);
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
cin >> f[i];
}
f[0] = 0;
f[n + 1] = n + 1;
while (1)
{
int maxx = 0, minn = inf;
for (rint i = 1; i <= n; i++)
{
maxx = max(maxx, f[i]);
if (maxx > f[i]) minn = min(minn, f[i]);
}
if (minn == inf) break;
int p = find(f + 1, f + n + 1, minn) - f;
int q = max_element(f + 1, f + p + 1) - f;
int k1 = find(f, f + n + 2, f[p] - 1) - f;
int k2 = find(f, f + n + 2, f[q] + 1) - f;
solve(k1 + 1, q, p, k2 - 1);
}
cout << ans.size() << endl;
for (auto i : ans)
{
int a, b, c, d;
tie(a, b, c, d) = i;
cout << a << " " << b << " " << c << " " << d << endl;
}
return 0;
}
CF1305G Kuroni and Antihype
将原问题转化,添加一个点权为 的点,将边权变为两端点点权和,将 减去 。原题意的生成有向森林转化为了生成树。
从大到小枚举边权和 ,枚举 的子集 ,将点权为 与 的点缩点并计算贡献。
有点卡常,但是不影响过。
const int N = 3e5 + 5;
int n, m;
int maxx;
int fa[N], cnt[N];
long long ans;
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int a, int b, long long w)
{
if (cnt[a] && cnt[b])
{
int x = find(a);
int y = find(b);
if (x != y)
{
ans += w * (cnt[x] + cnt[y] - 1);
fa[x] = y;
cnt[y] = 1;
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
cnt[0] = 1;
for (rint i = 1; i <= n; i++)
{
int x;
cin >> x;
cnt[x]++;
ans -= x;
maxx = max(maxx, x);
}
m = 1 << (32 - __builtin_clz(maxx));
for (rint i = 0; i < m; i++)
{
fa[i] = i;
}
for (rint i = m - 1; ~i; i--)
{
for (rint j = i; j; j = i & (j - 1))
{
if (cnt[j] && cnt[i ^ j])
{
merge(j, i ^ j, i);
}
}
merge(i, 0, i);
}
cout << ans << endl;
return 0;
}
[集训队互测 2023] Grievous Lady
由于最终结果选 的个数和选 的个数不会差很多,所以可以一开始的时候枚举 集合大小 在 之间。然后随机排列,将前 个选 ,后 个选 ,之后随机选数翻转,如果更优就翻。类似还有好多奇怪的随机化可以过。
当然,这道题还存在一种正解的 dp 做法, 表示选到第 个点, 的总和为 的最大 总和,之后再二维偏序,分析可以被扔掉的点可以做到只剩下 个状态。但是这个写法虽然比较稳定,但是太难写了,比较麻烦。随机化的得分会在 间波动。但是随机种子给的好可以稳过。(直接 srand(time(0))
过的概率也是很高的。
int n;
int A, B;
int a[N], b[N], p[N];
__int128 res;
bool v[N];
#define iint __int128
inline void write(iint n)
{
if (n < 0)
{
putchar('-');
n *= -1;
}
if (n > 9) write(n / 10);
putchar(n % 10 + '0');
}
#undef iint
void solve()
{
__int128 l = 0, r = 0;
for (rint i = 1; i <= n; i++)
{
if (!v[i]) l += a[i];
else r += b[i];
}
__int128 now = l * r;
int times = 4e4 + 5;
while (times--)
{
int x = rand() % n + 1;
if (!v[x])
{
if (now < ((l - a[x]) * (r + b[x])))
{
v[x] ^= 1;
l -= a[x];
r += b[x];
now = l * r;
}
}
else
{
if (now < (l + a[x]) * (r - b[x]))
{
v[x] ^= 1;
l += a[x];
r -= b[x];
now = l * r;
}
}
}
res = max(res, now);
}
void work()
{
for (rint i = 1; i <= n; i++)
{
cin >> a[i] >> b[i];
p[i] = i;
}
res = 0;
int times = 30;
while (times--)
{
random_shuffle(p + 1, p + n + 1);
for (rint i = 1; i <= n / 2; i++)
{
v[p[i]] = 1;
}
for (rint i = n / 2 + 1; i <= n; i++)
{
v[p[i]] = 0;
}
solve();
}
write(res);
cout << endl;
}
signed main()
{
srand(time(0));
int T;
cin >> T >> n >> A >> B;
while (T--) work();
return 0;
}
本文作者:PassName
本文链接:https://www.cnblogs.com/spaceswalker/p/17946064
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步