2024.07 别急记录
1. CEOI2023 - Balance
考虑 \(S=2\)。令 \((a_{i,j},j+T)\) 连一条无向边,若 \(a_{i,j}\) 度数为奇数则连 \((a_{i,j},S+T+1)\),在这张图上跑出一个欧拉回路,则对边进行定向后每个题目入度与出度相同,去掉点 \(S+T+1\) 后入度与出度差 \(1\),刚好符合题目要求。于是若欧拉回路中 \(j+T\to a_{i,j}\) 则令其放到左边一列,否则放到右边一列。
对于 \(S=2^k\) 的情况,相同处理后可以递归下去。
点击查看代码
// Problem: P9731 [CEOI2023] Balance
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P9731
// Memory Limit: 1 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
const int N = 6e5 + 10, M = 2e6 + 10;
int n, m, t;
vector<int> a[N];
int ind[N], cnt, id[N], cb[N];
int hd[N], fr[M], to[M], nx[M], tot = 1;
int vis[N], ban[M];
int cl[N], cr[N];
vector<int> st;
void add(int x, int y){
fr[++tot] = x;
to[tot] = y;
nx[tot] = hd[x];
hd[x] = tot;
}
void dfs(int x){
vis[x] = 1;
for(int &i = hd[x]; i; i = nx[i]){
int y = to[i];
if(ban[i]){
continue;
}
int eg = i;
ban[i] = ban[i^1] = 1;
dfs(y);
st.push_back(eg);
}
}
void solve(int l, int r){
if(l == r){
return;
}
int mid = l + r >> 1;
for(int i = 1; i <= n; ++ i){
for(int j = l; j <= r; ++ j){
if(!ind[a[i][j]]){
id[a[i][j]] = ++ cnt;
cb[cnt] = a[i][j];
}
++ ind[a[i][j]];
}
}
for(int i = 1; i <= n; ++ i){
for(int j = l; j <= r; ++ j){
add(id[a[i][j]], i+cnt);
add(i+cnt, id[a[i][j]]);
}
}
for(int i = 1; i <= cnt; ++ i){
if(ind[cb[i]] & 1){
add(i, n+cnt+1);
add(n+cnt+1, i);
}
}
for(int i = 1; i <= cnt + n + 1; ++ i){
if(!vis[i]){
dfs(i);
}
}
reverse(st.begin(), st.end());
for(int i = 1; i <= n; ++ i){
cl[i] = l, cr[i] = r;
}
for(auto i : st){
int x = fr[i], y = to[i];
if(x <= cnt && y <= n + cnt){
a[y-cnt][cl[y-cnt]++] = cb[x];
}
if(x <= n + cnt && y <= cnt){
a[x-cnt][cr[x-cnt]--] = cb[y];
}
}
vector<int> ().swap(st);
for(int i = 1; i <= cnt; ++ i){
ind[cb[i]] = id[cb[i]] = 0;
cb[i] = 0;
}
for(int i = 1; i <= cnt + n + 1; ++ i){
vis[i] = hd[i] = 0;
}
for(int i = 1; i <= tot; ++ i){
ban[i] = 0;
}
cnt = 0;
tot = 1;
solve(l, mid);
solve(mid+1, r);
}
int main(){
int T = 1;
while(T--){
scanf("%d%d%d", &n, &m, &t);
for(int i = 1; i <= n; ++ i){
a[i].resize(m+5);
for(int j = 1; j <= m; ++ j){
scanf("%d", &a[i][j]);
}
}
solve(1, m);
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= m; ++ j){
printf("%d ", a[i][j]);
}
puts("");
}
}
return 0;
}
2. CF1693C - Keshi in Search of AmShZ
题目转化为:给定一张有向图,求删边数+删边后最长路最小值。
考虑设 \(f_i\) 表示以 \(i\) 为起点的答案,有转移:
\(f_x=\min_{(x\to y)\in \mathbb E}\{f_y+1+\sum_{(x\to p)\in \mathbb E}[f_p>f_y]\}\)
即删去所有劣于 \((x,y)\) 的边并走未删去的最劣边 \((x,y)\)。
其实可以直接按照 dij 的方式进行转移,\(\sum_{(x\to p)\in \mathbb E}[f_p>f_y]\) 则用总出度减去小于的部分,这样就是正确的。
点击查看代码
//CF1693C
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m;
vector<int> g[N];
int vis[N];
int dis[N], ind[N];
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++ i){
int u, v;
scanf("%d%d", &u, &v);
g[v].push_back(u);
++ ind[u];
}
memset(dis, 0x3f, sizeof(dis));
priority_queue<pair<int, int> > q;
q.push(make_pair(0, n));
dis[n] = 0;
while(!q.empty()){
int x = q.top().second;
q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i : g[x]){
-- ind[i];
int val = dis[x] + ind[i] + 1;
if(dis[i] > val){
dis[i] = val;
q.push(make_pair(-dis[i], i));
}
}
}
printf("%d\n", dis[1]);
return 0;
}
3. CF1693D - Decinc Dividing
设 \(f_{i,0}\) 表示以 \(p_i\) 为递增序列结尾,递降序列结尾的最大值;\(f_{i,1}\) 则表示以 \(p_i\) 为递降序列结尾,递增序列结尾的最小值。
则有四种转移:
- 当 \(p_i<p_{i+1}\) 时 \(f_{i,0}\to f_{i+1,0}\);
- 当 \(f_{i,0}>p_{i+1}\) 时 \(p_i\to f_{i+1,1}\);
- 当 \(p_i>p_{i+1}\) 时 \(f_{i,1}\to f_{i+1,1}\);
- 当 \(f_{i,1}<p_{i+1}\) 时 \(p_i\to f_{i+1,0}\)。
那么初始化 \(f_{1,0}=\infty,f_{1,1}=-\infty\) 即可递推,枚举左端点递推即可做到 \(O(n^2)\)。
一个常见的思路是使用数据结构维护,同时进行多个左端点的转移;或者使用双指针确定区间范围。这里发现两种都不太好做,有一种 trick:令左端点从右往左,若一次递推 \(i\to i+1\) 不能改变 \(f_{i+1}\) 的值则直接退出,可以证明在这一题中这样转移复杂度是 \(O(n)\) 的。
我们考虑有解的 \(f_{i,0}\),对应的递降序列长度有 \(0,1,\geq 2\) 三种。对于改变值的情况一定是 \(0\to 1,1\to 2\),\(2\) 再在序列前面添加一个可以改变长度的数字就变成了无解,所以每个值变化次数是 \(O(1)\) 的。
当然这题也有数据结构维护的做法。
点击查看代码
//CF1693D
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, p[N], f[N][2];
long long ans = 0;
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &p[i]);
}
memset(f, -1, sizeof(f));
int la = n;
for(int i = n; i >= 1; -- i){
f[i][0] = n+1;
f[i][1] = 0;
for(int j = i; j < n; ++ j){
int f0 = 0, f1 = n+1;
if(p[j] < p[j+1]){
f0 = max(f0, f[j][0]);
}
if(f[j][0] > p[j+1]){
f1 = min(f1, p[j]);
}
if(p[j] > p[j+1]){
f1 = min(f1, f[j][1]);
}
if(f[j][1] < p[j+1]){
f0 = max(f0, p[j]);
}
if(f0 == f[j+1][0] && f1 == f[j+1][1]){
break;
}
f[j+1][0] = f0;
f[j+1][1] = f1;
if(f0 == 0 && f1 == n+1){
la = j;
break;
}
}
ans += la - i + 1;
}
printf("%lld\n", ans);
return 0;
}
4. AHOI2024 初中组 - 计数 / PA2021 - Od deski do deski
刚开始想 \(f_i\) 表示长度为 \(i\) 的序列的答案然后进行区间 dp,发现不好维护。
考虑对于一个序列如何判断可行:\(dp_i\) 表示 \([1,i]\) 是否可行,则 \(dp_i\to dp_j\) 当且仅当 \(i+1\neq j,a_{i+1}=a_j\)。
于是设 \(f_{i,j},g_{i,j}\) 表示长度为 \(i\) 的序列,有 \(j\) 种数存在 \(a_p=j\) 使得 \(dp_{p-1}=1\),整个区间是否合法的方案数,有四种转移:
- \(f_{i-1,j}*j\to g_{i,j}\),表示添加一个数使不可行变为可行。
- \(f_{i-1,j}*(m-j)\to f_{i,j}\),表示添加一个数仍旧不可行。因为 \(f_{i-1,j}\) 不可行所以 \(dp_{i-1}=0\),第二维的值不会改变(这也就是为什么要分 \(f,g\))。
- \(g_{i-1,j}*j\to g_{i,j}\),表示添加一个数仍旧可行。
- \(g_{i-1,j}*(m-j)\to f_{i,j+1}\),表示添加了一个另外的数使得可行变成不可行。因为 \(g_{i-1,j}\) 可行所以 \(dp_{i-1}=1\)。
点击查看代码
// Problem: P10375 [AHOI2024 初中组] 计数
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P10375
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
const int N = 3010;
typedef long long ll;
const ll P = 1e9 + 7;
ll f[N][N], g[N][N];
int n, m;
int main(){
int T = 1;
while(T--){
scanf("%d%d", &n, &m);
f[1][1] = m;
for(int i = 2; i <= n; ++ i){
for(int j = 0; j <= i; ++ j){
g[i][j] = (g[i][j] + f[i-1][j] * j) % P;
f[i][j] = (f[i][j] + f[i-1][j] * (m-j)) % P;
g[i][j] = (g[i][j] + g[i-1][j] * j) % P;
f[i][j+1] = (f[i][j+1] + g[i-1][j] * (m-j)) % P;
}
}
ll ans = 0;
for(int j = 1; j <= n; ++ j){
ans = (ans + g[n][j]) % P;
}
printf("%lld\n", ans);
}
return 0;
}
5. JOISC2014 - 歴史の研究
回滚莫队。之前不会现在想了想发现挺简单。
一样分块,两端在同一块内的暴力做。假设目前右端点 \(R_1\),下一个询问 \(L_2,R_2\),当前块右端点为 \(r_b\),则先从 \([r_b+1,R_1]\) 转移到 \([r_b+1,R_2]\),记录下此时的状态后再转移到 \([L_2,R_2]\),之后回滚到 \([r_b+1,R_2]\) 的状态即可。
点击查看代码
// Problem: 歴史の研究
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_joisc2014_c
// Memory Limit: 512 MB
// Time Limit: 4000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, q, a[N], b[N], bl;
int cnt[N];
ll ans[N];
struct qry{
int l, r, id;
} qr[N];
vector<qry> v[N];
bool cmp(qry x, qry y){
return x.r < y.r;
}
int main(){
int T = 1;
while(T--){
scanf("%d%d", &n, &q);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
b[i] = a[i];
}
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1; i <= n; ++ i){
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
}
bl = n / sqrt(q);
for(int i = 1; i <= q; ++ i){
scanf("%d%d", &qr[i].l, &qr[i].r);
qr[i].id = i;
v[(qr[i].l-1)/bl].push_back(qr[i]);
}
int mx = (n-1) / bl;
for(int i = 0; i <= mx; ++ i){
sort(v[i].begin(), v[i].end(), cmp);
int br = min(n, (i+1) * bl);
int r = br;
ll now = 0;
for(auto nw : v[i]){
if(nw.r <= br){
for(int j = nw.l; j <= nw.r; ++ j){
++ cnt[a[j]];
ans[nw.id] = max(ans[nw.id], (ll)b[a[j]] * cnt[a[j]]);
}
for(int j = nw.l; j <= nw.r; ++ j){
-- cnt[a[j]];
}
} else {
while(r < nw.r){
++ r;
++ cnt[a[r]];
now = max(now, (ll)b[a[r]] * cnt[a[r]]);
}
ans[nw.id] = now;
for(int j = br; j >= nw.l; -- j){
++ cnt[a[j]];
ans[nw.id] = max(ans[nw.id], (ll)b[a[j]] * cnt[a[j]]);
}
for(int j = br; j >= nw.l; -- j){
-- cnt[a[j]];
}
}
}
memset(cnt, 0, sizeof(cnt));
}
for(int i = 1; i <= q; ++ i){
printf("%lld\n", ans[i]);
}
}
return 0;
}
6. UNR #6 - 稳健型选手
100 分懒得写,来点 70。
考虑一个方案合法当且仅当区间内任意前缀先手取的至多比后手取的多 \(1\) 个,于是可以 dp。
考虑反悔贪心:先手取右侧接下来的两个,然后丢掉一个取过的最小值;同样也可以先手不取左侧接下来的两个,在取一个没取过的最大值。于是可以进行回滚莫队 \(O(n\sqrt q \log n)\) 求解。
点击查看代码
// Problem: C. 稳健型选手
// Contest: UOJ - UOJ NOI Round #6 Day1
// URL: https://uoj.ac/contest/77/problem/749
// Memory Limit: 512 MB
// Time Limit: 3000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n, q, Q, a[N], L[N], R[N];
ll ans[N];
struct qry{
int l, r, id;
} qr[N];
vector<int> v[N];
bool cmp(int x, int y){
return qr[x].r < qr[y].r;
}
pair<int, int> sta[N*4];
int tp;
priority_queue<int> ou, od;
priority_queue<int, vector<int>, greater<int> > in;
void solve(){
int bl = 510;
int mx = (n-1) / bl;
for(int i = 1; i <= q; ++ i){
v[(qr[i].l-1)/bl].push_back(i);
}
for(int p = 0; p <= mx; ++ p){
sort(v[p].begin(), v[p].end(), cmp);
int br = (p+1) * bl;
int r = br;
ll now = 0;
for(int pp : v[p]){
auto &x = qr[pp];
if(x.r <= br){
ll tmp = 0;
for(int i = x.r-1; i >= x.l; i -= 2){
ou.push(a[i]);
ou.push(a[i+1]);
tmp += ou.top();
ou.pop();
}
ou = {};
ans[x.id] += tmp;
} else {
while(r < x.r){
r += 2;
in.push(a[r-1]);
in.push(a[r]);
int x = in.top();
now += a[r-1] + a[r] - x;
ou.push(x);
in.pop();
}
od = ou;
ll tmp = now;
for(int i = br - 1; i >= x.l; i -= 2){
od.push(a[i]);
od.push(a[i+1]);
tmp += od.top();
od.pop();
}
ans[x.id] += tmp;
}
}
ou = {};
od = {};
in = {};
vector<int> ().swap(v[p]);
}
}
int main(){
scanf("%d%d", &n, &Q);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
}
for(int i = 1; i <= Q; ++ i){
scanf("%d%d", &L[i], &R[i]);
if((R[i] - L[i] + 1) & 1){
ans[i] += a[R[i]];
-- R[i];
}
}
q = 0;
for(int i = 1; i <= Q; ++ i){
if((L[i] & 1) && (L[i] <= R[i])){
qr[++q] = { L[i], R[i], i };
}
}
solve();
q = 0;
for(int i = 1; i <= Q; ++ i){
if((!(L[i] & 1)) && (L[i] <= R[i])){
qr[++q] = { L[i]-1, R[i]-1, i };
}
}
for(int i = 1; i < n; ++ i){
a[i] = a[i+1];
}
-- n;
solve();
for(int i = 1; i <= Q; ++ i){
printf("%lld\n", ans[i]);
}
return 0;
}
7. LG5591 - 小猪佩奇学数学
唉,单位根反演。
证明:
- 当 \(k\mid n\) 时 \(\omega_k^{tn}=1\),显然成立;
- 当 \(k\nmid n\) 时 右边构成 \((n,k)\) 个 \(\omega_{k/(n,k)}\) 的环,显然和为 \(0\),成立。
所以有:
由于 \(P=998244353\),所以 \(\omega_k^1\) 直接取 \(3^{\frac{P-1}k}\) 即可。
点击查看代码
// Problem: P5591 小猪佩奇学数学
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5591
// Memory Limit: 500 MB
// Time Limit: 3000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 998244353, iG = 3;
const int N = (1<<20) + 10;
ll n, p, k, w[N];
ll qp(ll x, ll y){
x %= P;
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
scanf("%lld%lld%lld", &n, &p, &k);
w[0] = 1;
w[1] = qp(iG, (P-1) / k);
for(int i = 2; i < k; ++ i){
w[i] = w[i-1] * w[1] % P;
}
ll ans = 0;
for(int t = 1; t < k; ++ t){
ll tmp = (qp(p+1, n) + P - qp(p*w[t]+1, n) * w[t] % P) % P;
ans = (ans + tmp * qp(1+P-w[t], P-2)) % P;
}
ans = (ans + n * p % P * qp(p+1, n-1) + qp(p+1, n)) % P;
ans = ans * qp(k, P-2) % P;
ans = (ans + P - qp(p+1, n)) % P;
printf("%lld\n", ans);
return 0;
}
8. loj6485 - LJJ 学二项式定理
同上一题使用单位根反演得答案为:
点击查看代码
// Problem: #6485. LJJ 学二项式定理
// Contest: LibreOJ
// URL: https://loj.ac/p/6485
// Memory Limit: 256 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 998244353, iG = 3;
ll n, s, a[4], w[4];
ll qp(ll x, ll y){
x %= P;
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
w[0] = 1;
w[1] = qp(iG, (P-1) / 4);
w[2] = w[1] * w[1] % P;
w[3] = w[2] * w[1] % P;
int T = 1;
scanf("%d", &T);
while(T--){
scanf("%lld%lld%lld%lld%lld%lld", &n, &s, &a[0], &a[1], &a[2], &a[3]);
ll ans = 0;
for(int i = 0; i <= 3; ++ i){
for(int j = 0; j <= 3; ++ j){
ans = (ans + a[i] * w[(4-i*j%4)%4] % P * qp(s*w[j]+1, n)) % P;
}
}
printf("%lld\n", ans * qp(4, P-2) % P);
}
return 0;
}
9. UNR #1 - Jakarta Skyscrapers
首先可以实现一个操作:给定一个 \(y\) 和一个 \(x > y*k\),构造出 \(y*k\),显然可以如下构造:
- \({x,y} \to {x,y,x-y}\to{x,y,x-y,x-2y}\to{x,y,2y,x-y,x-2y}\),递归下去构造出所有需要的 \(2^py\)。
- 把 \(k\) 二进制拆分,构造 \({x,x-2^{p_1}y,x-2^{p_1}y-2^{p_2}y,...,x-ky}\)。
然后就可以先用辗转相除构造出 \(1\),然后构造 \(1*c\) 即可。
无解情况:\((a,b)\nmid c\) 或 \(c>\max(a, b)\)。
点击查看代码
// Problem: A. Jakarta Skyscrapers
// Contest: UOJ - UOJ NOI Round #1 Day2
// URL: https://uoj.ac/contest/32/problem/216
// Memory Limit: 256 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a, b, c;
map<ll, int> mp;
pair<ll, ll> ans[500];
int cnt = 0;
void add(ll x, ll y){
if(!mp[x] || !mp[y]){
printf("waa %d %d %d %d\n", x, mp[x], y, mp[y]);
}
if(!mp[x-y]){
mp[x-y] = 1;
ans[++cnt] = make_pair(x, y);
}
}
void bld(ll x, ll y, ll k){
add(x, y);
if(k == 1) return;
for(ll i = 1; i*2 <= k; i *= 2){
add(x-y*i, y*i);
add(x, x-y*i*2);
}
ll nw = x;
for(int i = 62; i >= 0; -- i){
if(k & (1ll << i)){
add(nw, y*(1ll<<i));
nw -= y*(1ll << i);
}
}
add(x, nw);
}
void zz(ll x, ll y){
// printf("%d %d %d\n", x, y, x/y);
if(y == 1 || x == 1){
return;
}
bld(x, y, x/y);
zz(y, x%y);
}
int main(){
cin >> a >> b >> c;
if(a < b){
swap(a, b);
}
if(a == c || b == c){
puts("0");
return 0;
}
ll g = __gcd(a, b);
if(c % g || c > a){
puts("-1");
return 0;
}
a /= g;
b /= g;
c /= g;
mp[a] = mp[b] = 1;
zz(a, b);
bld(a, 1, a-c);
printf("%d\n", cnt);
for(int i = 1; i <= cnt; ++ i){
printf("%lld %lld\n", ans[i].first * g, ans[i].second * g);
}
return 0;
}
10. UNR #8 - 兵棋
不妨串的第一位为 \(\texttt 0\),则若将串的末尾添加足够多的 \(\texttt 0\),每次操作就会形如:找到所有 \(\texttt{10}\) 的位置,删掉这两个字符。把 \(\texttt 0\) 看做向上的折线,\(\texttt 1\) 看做向下的折线,则 \(k\) 次操作就相当于在折线上方形成若干水坑,每个水坑深度 \(\leq k\),被淹没的字符就被删除。于是设 \(f/g_{i,j}\) 表示前 \(i\) 个字符,距离水面 \(j\) 的权值/方案数,可以直接 dp。
点击查看代码
// Problem: A. 兵棋
// Contest: UOJ - UOJ NOI Round #8 Day2
// URL: https://uoj.ac/contest/91/problem/890
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 6, K = 204;
const int P = 998244353;
typedef long long ll;
int n, k;
char s[N];
int f[N][K], g[N][K];
void upd(int &x, int y){
x += y;
if(x >= P){
x -= P;
}
}
int calc(){
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
f[1][0] = g[1][0] = 1;
for(int i = 2; i <= n; ++ i){
for(int j = 0; j <= k; ++ j){
if(s[i] == '?' || s[i] == '0'){
if(j){
upd(g[i][j-1], g[i-1][j]);
upd(f[i][j-1], f[i-1][j]);
} else {
upd(g[i][0], g[i-1][0]);
upd(f[i][0], f[i-1][0]);
upd(f[i][0], g[i-1][0]);
}
}
if(s[i] == '?' || s[i] == '1'){
if(j < k){
upd(g[i][j+1], g[i-1][j]);
upd(f[i][j+1], f[i-1][j]);
} else {
upd(g[i][k], g[i-1][k]);
upd(f[i][k], f[i-1][k]);
upd(f[i][k], g[i-1][k]);
}
}
}
}
int ans = 0;
for(int i = 0; i <= k; ++ i){
upd(ans, f[n][i]);
}
return ans;
}
int main(){
scanf("%d%d", &n, &k);
scanf("%s", s+1);
int ans = 0;
if(s[1] != '1'){
upd(ans, calc());
}
if(s[1] != '0'){
for(int i = 1; i <= n; ++ i){
if(s[i] == '0'){
s[i] = '1';
} else if(s[i] == '1'){
s[i] = '0';
}
}
upd(ans, calc());
}
printf("%d\n", ans);
return 0;
}
11. BZOJ3028/LGP10780 - 食物
考虑生成函数,\(ans=[x^n]F(x)\),其中:
化简得 \(F(x)=\dfrac x{(1-x)^4}=x\sum\dbinom{3+i}ix^i=\sum\dbinom{i+2}{i-1}x^i\)。
即答案为 \(\dfrac {n(n+1)(n+2)}6\)。
点击查看代码
// Problem: P10780 BZOJ3028 食物
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P10780
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 510 * 4;
const ll P = 10007;
int n;
char t[N];
int s[3][N];
void dv(int x, int v){
for(int i = n-1; i >= 0; -- i){
s[x][i-1] += s[x][i] % v * 10;
s[x][i] /= v;
}
}
ll clc(int x){
ll ans = 0;
for(int i = n-1; i >= 0; -- i){
ans = (ans * 10 + s[x][i]) % P;
}
return ans;
}
int main(){
scanf("%s", t);
n = strlen(t);
for(int i = 0; i < n; ++ i){
s[0][i] = s[1][i] = s[2][i] = t[n-i-1] - '0';
}
s[1][0] += 2;
s[2][0] += 1;
int sm = 0;
for(int i = 0; i < n; ++ i){
sm += s[0][i];
}
dv((s[0][0]&1)*2, 2);
dv(sm%3, 3);
printf("%lld\n", clc(0) * clc(1) * clc(2) % P);
return 0;
}
12. LGP2012 - 拯救世界2
由于这题序列有序,所以不能使用 OGF,应使用 EGF。
EGF 定义:\(F(x)=\sum\limits_{i=0}^{\infty}\dfrac {x^i}{i!}a_i\)。则有 \(a_n=n![x^n]F(x)\)。
为什么这样定义?考虑 \(p,q,r\) 个 \(\texttt{a,b,c}\) 拼成的字符串数,由 EGF 得 \(G(x)=\dfrac{x^p}{p!}\dfrac{x^q}{q!}\dfrac{x^r}{r!}=\dfrac{x^{p+q+r}}{p!q!r!}\),答案 \((p+q+r)![x^{p+q+r}G(x)]=\dfrac{(p+q+r)!}{p!q!r!}\),恰好为可重集公式,即答案。
那么原题的 EGF 分别为:
- ACGT:\(F_1(x)=\sum\dfrac {x^i}{i!}=e^x\);
- 阳:\(F_2(x)=\sum\dfrac {x^{2i+1}}{(2i+1)!}=\dfrac 12(e^x-e^{-x})\);
- 阴:\(F_3(x)=\sum\dfrac {x^{2i}}{(2i)!}=\dfrac 12(e^x+e^{-x})\);
\(G(x)=(F_1(x)F_2(x)F_3(x))^4=\dfrac 1{256}(e^{12x}-4e^{8x}+6e^{4x}-4+e^{-4x})\)。
答案为 \(n![x^n]G(x)=\dfrac 1{256}(12^n-4\times 8^n+6\times 4^n+(-4)^n)\)。
点击查看代码
// Problem: P2012 拯救世界2
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2012
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 1e9;
ll n;
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
while(scanf("%lld", &n) && n){
if(n <= 6){
ll ans = qp(12, n) - 4 * qp(8, n) + 6 * qp(4, n) + qp(-4, n);
ans /= 256;
printf("%lld\n", ans % P);
} else {
n -= 4;
ll ans = 81 * qp(12, n) - 64 * qp(8, n) + 6 * qp(4, n) + qp(P-4, n);
printf("%lld\n", (ans % P + P) % P);
}
}
return 0;
}
13. LG4844 - LJJ爱数数
由题意得 \(ab-ac-bc=0\)。
两边同时加 \(c^2\) 得 \((a-c)(b-c)=c^2\)。很难不证明此时 \((a-c, b-c)=1\),设 \(a-c = p^2,b-c = q^2\),则一对 \((p,q)\) 能对答案产生贡献当且仅当 \((p,q)=1,pq+\max^2(p,q)\leq n\)。很难不观察到 \(p=q\) 的答案仅有 \((2,2,1)\) 一组,所以很难求不出 \(p>q\) 的答案再 \(*2+1\)。
\(\sum_{p=1}^n\sum_{q=1}^{\min(p-1,n/p-p)}[(p,q)=1]\)。
考虑莫反,很难不求出答案为 \(\sum_{d=1}^n\mu(d)\sum_{i=1}^{n/d}\max(0,\min(di-1,\dfrac n{di}-di))\)。
很难不发现后面式子中含有 \(d\) 不能数论分块,但很难不发现对于答案有贡献的 \(i,d\leq \sqrt n\)。所以枚举 \(\sqrt n\) 内的 \(i,d\) 即可做到复杂度 \(O(\sqrt n\log\sqrt n)\)。
点击查看代码
// Problem: P4844 LJJ爱数数
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4844
// Memory Limit: 250 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
ll n;
int mu[N], p[N], c, v[N];
int main(){
n = 1e6;
mu[1] = 1;
for(int i = 2; i <= n; ++ i){
if(!v[i]){
p[++c] = i;
mu[i] = -1;
}
for(int j = 1; j <= c && i * p[j] <= n; ++ j){
v[i*p[j]] = 1;
if(i % p[j]){
mu[i*p[j]] = -mu[i];
} else {
mu[i*p[j]] = 0;
break;
}
}
}
cin >> n;
ll m = sqrt(n);
ll ans = 0;
for(ll d = 1; d <= m; ++ d){
ll tmp = 0;
for(ll i = 1; i <= m/d; ++ i){
tmp += min(i-1, n/i/d/d-i);
}
ans += tmp * mu[d];
}
cout << ans * 2 + 1;
return 0;
}
14. HNOI2014 - 江南乐
考虑 \(sg(i)=\operatorname{mex}_{m=2}\{[sg(\lfloor\dfrac xm \rfloor)\times((m-x\bmod m)\bmod 2)]\operatorname{xor}[sg(\lfloor\dfrac xm \rfloor+1)\times((x\bmod m)\bmod 2)]\}\)。
考虑数论分块,对于 \(\lfloor\dfrac xm \rfloor\) 相同的一块,易证只用取块中连续两个 \(m\) 的 \(sg\) 值进行更新即可做到正确。打表发现 \(sg(i)\leq 17\),所以可以做到 \(O(1)\) 查询 \(\operatorname{mex}\)。总复杂度 \(O(n\sqrt n)\)。
点击查看代码
// Problem: P3235 [HNOI2014] 江南乐
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3235
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
int n, F, f[N];
int calc(int i, int m){
int tmp = 0;
if((m - i % m) & 1){
tmp ^= f[i/m];
}
if((i % m) & 1){
tmp ^= f[i/m+1];
}
return tmp;
}
int main(){
int T = 1;
scanf("%d%d", &T, &F);
for(int i = 0; i < F; ++ i){
f[i] = 0;
}
n = 100000;
for(int i = F; i <= n; ++ i){
int v = 0;
for(int l = 2, r; l <= i; l = r + 1){
r = i / (i / l);
v |= (1 << calc(i, l));
if(l != r){
v |= (1 << calc(i, l+1));
}
}
for(int j = 0; j < 20; ++ j){
if(!(v & (1 << j))){
f[i] = j;
break;
}
}
}
while(T--){
scanf("%d", &n);
int ans = 0, a = 0;
while(n--){
scanf("%d", &a);
ans ^= f[a];
}
printf("%d ", ans ? 1 : 0);
}
puts("");
return 0;
}
15. CF1443E - Long Permutation
发现 \(\sum qx\leq 2*10^{10}< 15!\),所以每次修改至多只会改变排列末尾 \(15\) 位的值。所以可以每次修改暴力逆康托展开,询问时将排列分成 \([1,n-15],[n-14,n]\) 两段分开询问即可。
点击查看代码
// Problem: Long Permutation
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF1443E
// Memory Limit: 250 MB
// Time Limit: 4000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n, q, la[25];
ll sum = 1, fac[25];
void upd(int len, ll val){
static int k[25];
memset(k, 0, sizeof(k));
ll nw = 1;
for(int i = 1; i <= len; ++ i){
int p = 1;
while(nw + fac[len-i] <= val){
nw += fac[len-i];
++ p;
}
for(int j = 1, c = 0; j <= len; ++ j){
if(!k[j]){
++ c;
if(c == p){
la[i] = j;
k[j] = 1;
}
}
}
}
}
ll calc(int x){
if(x < n - 15){
return (ll)x * (x+1) / 2;
} else {
ll ans = (ll)(n-16) * (n-15) / 2;
for(int i = n-15; i <= x; ++ i){
ans += la[i-n+16] + (n-16);
}
return ans;
}
}
int main(){
fac[0] = 1;
for(int i = 1; i < 20; ++ i){
fac[i] = fac[i-1] * i;
}
scanf("%d%d", &n, &q);
if(n <= 16){
upd(n, sum);
} else {
upd(16, sum);
}
while(q--){
int op, x, l, r;
scanf("%d", &op);
if(op == 1){
scanf("%d%d", &l, &r);
ll ans = 0;
if(n <= 16){
for(int i = l; i <= r; ++ i){
ans += la[i];
}
} else {
ans = calc(r) - calc(l-1);
}
printf("%lld\n", ans);
} else {
scanf("%d", &x);
sum += x;
if(n <= 16){
upd(n, sum);
} else {
upd(16, sum);
}
}
}
return 0;
}
16. CF1700E - Serega the Pirate
发现一个合法的矩阵等价于对于一个 \(a_{x,y}>1\),有这个格子周围四个中存在一个小于它的数字。
观察到对于一个不合法的格子,它或它的四周一定有个格子要被换掉,所以可以枚举这五个格子以及另一个被换掉的格子,然后计算这两个格子交换后是否合法。具体可以记录每个格子是否合法以及总合法格子数,每次交换后暴力询问这两个格子周围格子状态是否改变。
点击查看代码
<details>
<summary>点击查看代码</summary>
```cpp
// Problem: Serega the Pirate
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF1700E
// Memory Limit: 250 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 4e5 + 10;
const int dx[5] = {-1, 0, 0, 1, 0}, dy[5] = {0, -1, 1, 0, 0};
vector<int> a[N], ok[N], vs[N];
int n, m, cnt;
bool chk(int x, int y){
if(x <= 0 || y <= 0 || x > n || y > m){
return 0;
}
for(int k = 0; k < 4; ++ k){
if(a[x+dx[k]][y+dy[k]] < a[x][y]){
return 1;
}
}
if(a[x][y] == 1){
return 1;
}
return 0;
}
bool chg(int px, int py, int x, int y){
swap(a[px][py], a[x][y]);
int tp = 0, tmp = cnt;
for(int i = 0; i <= 4; ++ i){
int p = px + dx[i], q = py + dy[i];
if(!vs[p][q]){
vs[p][q] = 1;
if(chk(p, q) && !ok[p][q]){
++ tmp;
} else if(!chk(p, q) && ok[p][q]){
-- tmp;
}
}
p = x + dx[i], q = y + dy[i];
if(!vs[p][q]){
vs[p][q] = 1;
if(chk(p, q) && !ok[p][q]){
++ tmp;
} else if(!chk(p, q) && ok[p][q]){
-- tmp;
}
}
}
for(int i = 0; i <= 4; ++ i){
vs[px+dx[i]][py+dy[i]] = 0;
vs[x+dx[i]][y+dy[i]] = 0;
}
swap(a[px][py], a[x][y]);
return tmp == n*m;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i <= n + 1; ++ i){
a[i].resize(m+2);
ok[i].resize(m+2);
vs[i].resize(m+2);
for(int j = 0; j <= m + 1; ++ j){
a[i][j] = 1e9;
ok[i][j] = 0;
}
}
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= m; ++ j){
scanf("%d", &a[i][j]);
}
}
int px, py;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= m; ++ j){
ok[i][j] = chk(i, j);
if(a[i][j] == 1){
ok[i][j] = 1;
}
if(ok[i][j]){
++ cnt;
} else {
px = i;
py = j;
}
}
}
if(cnt == n * m){
puts("0");
} else {
int ans = 0;
for(int k = 0; k <= 4; ++ k){
int ppx = px + dx[k], ppy = py + dy[k];
if(ppx <= 0 || ppy <= 0 || ppx > n || ppy > m){
continue;
}
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= m; ++ j){
bool flg = 1;
for(int l = 0; l <= k; ++ l){
if(i == px + dx[l] && j == py + dy[l]){
flg = 0;
break;
}
}
if(flg){
ans += chg(ppx, ppy, i, j);
}
}
}
}
if(ans){
printf("1 %d\n", ans);
} else {
puts("2");
}
}
return 0;
}
17. CF1849F - XOR Partition
考虑 \((i,j)\) 连一条边权为 \(a_i\operatorname{xor}a_j\) 的边,那么一个值能作为答案当且仅当小于这个值的边形成一张二分图。从小往大加边,显然一棵最小生成树将点黑白染色后的分类方法是最优的。所以只用求出最小异或生成树后输出黑白染色的方案即可,使用 boruvka 算法解决。
点击查看代码
// Problem: XOR Partition
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF1849F
// Memory Limit: 500 MB
// Time Limit: 7000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200010, M = 31;
int n, a[N];
int ch[N*M][2], siz[N*M], tot, id[N*M];
int fa[N];
vector<int> son[N], g[N];
int cl[N];
void ins(int x, int v){
int p = 0;
for(int i = 29; i >= 0; -- i){
if(!ch[p][(a[x]>>i)&1]){
ch[p][(a[x]>>i)&1] = ++ tot;
}
p = ch[p][(a[x]>>i)&1];
siz[p] += v;
}
id[p] = x;
}
int qry(int x){
int p = 0;
for(int i = 29; i >= 0; -- i){
if(siz[ch[p][(x>>i)&1]]){
p = ch[p][(x>>i)&1];
} else {
p = ch[p][1-((x>>i)&1)];
}
}
return id[p];
}
int gf(int x){
return x == fa[x] ? x : gf(fa[x]);
}
void mg(int x, int y){
x = gf(x);
y = gf(y);
if(son[x].size() < son[y].size()){
swap(x, y);
}
fa[y] = x;
for(int j : son[y]){
son[x].push_back(j);
}
}
void dfs(int x, int fa, int c){
cl[x] = c;
for(int i : g[x]){
if(i != fa){
dfs(i, x, 1-c);
}
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
ins(i, 1);
fa[i] = i;
son[i].push_back(i);
}
int cnt = 0;
while(cnt < n - 1){
static int gx[N], gy[N];
for(int i = 1; i <= n; ++ i){
if(i == gf(i)){
int vl = 2147483647;
for(int j : son[i]){
ins(j, -1);
}
for(int j : son[i]){
int k = qry(a[j]);
// printf("%d %d\n", j, k);
if(vl > (a[j] ^ a[k])){
vl = a[j] ^ a[k];
gx[i] = j;
gy[i] = k;
}
}
for(int j : son[i]){
ins(j, 1);
}
}
}
for(int i = 1; i <= n; ++ i){
if(i == gf(i) && gf(gx[i]) != gf(gy[i])){
mg(gx[i], gy[i]);
g[gx[i]].push_back(gy[i]);
g[gy[i]].push_back(gx[i]);
++ cnt;
}
}
}
dfs(1, 0, 1);
for(int i = 1; i <= n; ++ i){
printf("%d", cl[i]);
}
puts("");
return 0;
}
18. LGP7173 - 【模板】有负圈的费用流
先流满负权边,建流量相同费用相反的反边即可。
步骤:
- 使用上下界网络流的 trick 流满负权边,建流量相同费用相反的反边;
- 建超级源、汇点 \(S,T\) 相关的边,建 \((t,s,\infty,0)\)。
- 跑 \(S\to T\) 的 mcmf,记录此时的最大流为 \((s,t)\) 边的流量;
- 删掉图中的所有附加边;
- 跑 \(s\to t\) 的 mcmf,最大流为此时的流量加上第一次 mcmf 时记录的流量;
- 最小费用为所有 mcmf 时刻的费用和。
点击查看代码
// Problem: P7173 【模板】有负圈的费用流
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7173
// Memory Limit: 128 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 210, M = 1e5, inf = 1e9;
int n, m, s, t, nd[N], mf, mc;
int hd[N], eg[M], ln[M], cs[M], nx[M], tot = 1;
int now[N], dis[N], vis[N];
void add(int u, int v, int w, int c){
eg[++tot] = v;
ln[tot] = w;
cs[tot] = c;
nx[tot] = hd[u];
hd[u] = tot;
}
bool spfa(int s, int t){
queue<int> q;
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
memcpy(now, hd, sizeof(hd));
dis[s] = 0;
vis[s] = 1;
q.push(s);
while(!q.empty()){
int x = q.front();
q.pop();
vis[x] = 0;
for(int i = hd[x]; i; i = nx[i]){
int y = eg[i], z = ln[i], c = cs[i];
if(z && dis[y] > dis[x] + c){
dis[y] = dis[x] + c;
if(!vis[y]){
vis[y] = 1;
q.push(y);
}
}
}
}
return dis[t] != 0x3f3f3f3f;
}
int dfs(int x, int t, int fl){
if(x == t){
return fl;
}
vis[x] = 1;
int rs = fl;
for(int i = now[x]; i && rs; i = nx[i]){
int y = eg[i], z = ln[i], c = cs[i];
if(!vis[y] && z && dis[y] == dis[x] + c){
int k = dfs(y, t, min(rs, z));
if(!k){
dis[y] = inf;
}
ln[i] -= k;
ln[i^1] += k;
rs -= k;
mc += k * c;
}
}
return fl - rs;
}
int dinic(int s, int t){
int mf = 0, tmp;
while(spfa(s, t)){
while(tmp = dfs(s, t, inf)){
mf += tmp;
}
}
return mf;
}
int main(){
scanf("%d%d%d%d", &n, &m, &s, &t);
int S = n + 1, T = n + 2;
for(int i = 1; i <= m; ++ i){
int u, v, w, c;
scanf("%d%d%d%d", &u, &v, &w, &c);
if(c > 0){
add(u, v, w, c);
add(v, u, 0, -c);
} else {
nd[u] -= w;
nd[v] += w;
add(v, u, w, -c);
add(u, v, 0, c);
mc += c * w;
}
}
int tag = tot;
for(int i = 1; i <= n; ++ i){
if(nd[i] > 0){
add(S, i, nd[i], 0);
add(i, S, 0, 0);
} else {
add(i, T, -nd[i], 0);
add(T, i, 0, 0);
}
}
add(t, s, inf, 0);
add(s, t, 0, 0);
dinic(S, T);
mf += ln[tot];
for(int i = tag + 1; i <= tot; ++ i){
ln[i] = 0;
}
mf += dinic(s, t);
printf("%d %d\n", mf, mc);
return 0;
}
19. PKUSC2024 - 分流器
题意转化为至少多少次操作使得每个点都被操作偶数次。不妨模拟 \(1\) 号点被操作 \(2^{k}\) 次,其中 \(k\) 足够大,然后模拟出来每个点都被操作了几次。具体方法是使用压位高精维护每个点的操作次数(设为 \(a_i\)),每次转移令 \(a_{ls},a_{rs}\) 都加上 \(\dfrac{a_i}2\)。最后答案就是 \(\max p\) 使得 \(2|\dfrac {a_i}p\)。
点击查看代码
//qoj8671
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 50010;
int n, ch[N][2], mn = 10000000;
void pr(int x){
static int a[N];
memset(a, 0, sizeof(a));
int len = 1;
a[1] = 1;
while(true){
if(x >= 8){
for(int i = 1; i <= len; ++ i){
a[i] *= (1 << 8);
}
for(int i = 1; i <= len; ++ i){
a[i+1] += a[i] / 10;
a[i] %= 10;
}
while(a[len+1]){
++ len;
a[len+1] += a[len] / 10;
a[len] %= 10;
}
x -= 8;
} else {
for(int i = 1; i <= len; ++ i){
a[i] *= (1 << x);
}
for(int i = 1; i <= len; ++ i){
a[i+1] += a[i] / 10;
a[i] %= 10;
}
while(a[len+1]){
++ len;
a[len+1] += a[len] / 10;
a[len] %= 10;
}
break;
}
}
for(int i = len; i >= 1; -- i){
putchar(a[i]+'0');
}
}
const ll P = (1ll << 50);
struct bigint{
ll a[1010];
int qmn(){
for(int i = 0; i <= 1000; ++ i){
a[i+1] += a[i] / P;
a[i] %= P;
}
for(int i = 0; i <= 1000; ++ i){
if(a[i]){
for(ll j = 0; j < 50; ++ j){
if(a[i] & (1ll << j)){
return i * 50 + j;
}
}
}
}
}
void dv2(){
for(int i = 1000; i >= 0; -- i){
if(a[i] & 1){
a[i-1] += P;
}
a[i] >>= 1;
}
}
} a[N];
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d%d", &ch[i][0], &ch[i][1]);
}
a[1].a[1000] |= (1 << 10);
for(int i = 1; i <= n; ++ i){
mn = min(mn, a[i].qmn());
a[i].dv2();
for(int j = 0; j <= 1000; ++ j){
a[ch[i][0]].a[j] += a[i].a[j];
a[ch[i][1]].a[j] += a[i].a[j];
}
}
mn = min(mn, a[n+1].qmn());
pr(50011 - mn);
return 0;
}
20. LGP6114 - 【模板】Lyndon 分解
定义一个串是 lyndon 串:
这个串严格最小后缀是它本身。
这个串是它所有循环同构的串中字典序严格最小的。
两种定义等价,证明:
设串 \(S=T_1T_2\)(均非空)。
定义一推出定义二:\(S<T_2<T_2T_1\)。
定义二推出定义一:\(S<T_2T_1\),若 \(T_2\) 不为 \(S\) 前缀,则 \(S<T_2\),否则 \(T_2\) 为 \(S\) 前缀且为 \(S\) border,易证矛盾。
定义一个串的 lyndon 分解 \(S=T_1T_2...T_k\):
\(T_1,T_2,...,T_k\) 均为 lyndon 串且 \(T_i\geq T_{i+1}\)。
可以证明这样的分解存在且唯一:
引理:若 \(U,V\) 是 lyndon 串且 \(U<V\),则 \(UV\) 是 lyndon 串。
证明:若 \(|U|\geq|V|\) 则 \(UV<V<suf(V)\);若 \(|U|<|V|\) 且 \(U\) 不为 \(V\) 前缀则 \(U<UV<V\);否则若 \(U\) 为 \(V\) 前缀且 \(V<UV\) 则有 \(V\) 的一个后缀 \(<V\),矛盾。
存在性:由引理,可以初始化每个 lyndon 子串为单字符,然后合并直至 \(T_i\geq T_{i+1}\)。
唯一性:若 \(S=T_1T_2...T_gT_{g+1}...=T_1T_2...T_g^{1}T_{g+1}^{1}...\),不妨 \(|T_g|>|T_g^{1}|\),设 \(T_g=T_g^{1}T_{g+1}^{1}...pre(T_k)\),则 \(T_g<pre(T_k)\leq T_k\leq T_g^{1}<T_g\),矛盾。
引理:若 \(Sc\) 为一个 lyndon 串前缀,\(d>c\),则有 \(Sd\) 为 lyndon 串。
证明
\(suf(S)d>suf(S)c>Sc,d>c>S\)。
线性求 lyndon 分解的 Duval 算法:
维护 \(i,j,k\) 表示:
- \(pre(S,i-1)=T_1T_2...T_g\) 表示已经分解了 \([1,i-1]\) 的字符。
- \(S[i,k-1]=T^hV\)(\(T\) 为 lyndon 串,\(V\in pre(T)\neq T\)),\(j=k-|T|\)。
(from wucstdio)
考虑 \(S_k\):
- 若 \(S_k=S_j\),则继续匹配;
- 若 \(S_k>S_j\),则由引理得 \(T^hVS_k\) 为一个 lyndon 串,合并为一个 \(T\);
- 若 \(S_k<S_j\),则固定 \(T^h\) 分为 \(h\) 个 lyndon 串,从 \(V\) 开头处重新开始分解。
点击查看代码
// Problem: P6114 【模板】Lyndon 分解
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6114
// Memory Limit: 500 MB
// Time Limit: 300000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e6 + 10;
int n, ans;
char s[N];
int main(){
scanf("%s", s+1);
n = strlen(s+1);
for(int i = 1; i <= n;){
int j = i, k = i+1;
while(k <= n && s[j] <= s[k]){
if(s[j] < s[k]){
j = i;
} else {
++ j;
}
++ k;
}
while(i <= j){
ans ^= i + (k - j - 1);
i += k - j;
}
}
printf("%d\n", ans);
return 0;
}
21. LGP1368 - 【模板】最小表示法
首先对串 \(SS\) 做 lyndon 分解,易证最小表示法的左端点为划分中 \(n\in [l,r]\) 中的 \(l\)。
点击查看代码
// Problem: P1368 【模板】最小表示法
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1368
// Memory Limit: 250 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 6e5 + 10;
int n, a[N], r[N], tot;
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
a[i+n] = a[i];
}
for(int i = 1; i <= n + n;){
int j = i, k = i + 1;
while(k <= n + n && a[j] <= a[k]){
if(a[j] < a[k]){
j = i;
} else {
++ j;
}
++ k;
}
while(i <= j){
r[++tot] = i+k-j-1;
i += k-j;
}
}
for(int i = 1; i <= n+n; ++ i){
if(r[i] >= n){
for(int j = r[i-1]+1, k = 1; k <= n; ++ j, ++ k){
printf("%d ", a[j]);
}
puts("");
return 0;
}
}
}
22. some string problem
题意:给一个串 \(S\),要求把它分成 \(S=T_1T_2...T_k\),然后任意翻转(rev)其中若干项 \(T\),求翻转后字典序最小串。
可以发现一定有一个最优解是把每个 \(T\) 都翻转的,因为可以把不翻转的一个 \(T\) 拆成若干个长度为 \(1\) 的串再翻转。从左往右考虑每个字符,那么这个字符要把右侧一个最小的串拽过来,发现和 lyndon 类似。于是考虑求出反串的 lyndon 分解,然后按照这个分解划分原串即可。
23. CTT2012 - 楼房重建
经典 trick:线段树维护前缀最大值数量。
考虑一栋楼可以被看见当且仅当 \(a_i\neq 0\) 且对于任意 \(1\leq j<i\) 有 \(\dfrac{a_i}i>\dfrac{a_j}j\),相当于前缀严格最大值个数。
考虑线段树每个节点维护区间最大值 \(mx\)、以及不考虑这个区间以外部分时区间右端点的答案 \(cnt\)。
两个区间合并时:
- \(mx\) 直接合并;
- \(cnt\) 可用左子树答案加上右子树在左子树 \(mx\) 影响下的答案(记作 \(calc(rs,mx)\))。
接下来实现 \(calc(p, h)\) 表示子树 \(p\) 在前面有一个 \(h\) 的影响下的答案:
- 若 \(h\leq mx_{ls}\),则右子树答案不会因 \(h\) 改变,返回 \(calc(ls,h)+cnt_p\)(\(cnt_p\) 定义是右子树答案);
- 否则左子树不会有贡献,返回 \(calc(rs,h)\)。
点击查看代码
//qoj3679
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
typedef long long ll;
int n, m, a[N];
struct node{
int cnt, id;
} t[N*4];
bool geq(int x, int y){
if(!y){
return a[x];
}
return ((ll)a[x] * y > (ll)a[y] * x);
}
void build(int p, int l, int r){
t[p].cnt = 1;
t[p].id = l;
if(l != r){
int mid = l + r >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid+1, r);
}
}
int calc(int p, int l, int r, int h){
if(l == r){
return geq(l, h);
} else {
int mid = l + r >> 1;
if(geq(t[p<<1].id, h)){
return calc(p<<1, l, mid, h) + t[p].cnt;
} else {
return calc(p<<1|1, mid+1, r, h);
}
}
}
void mdf(int p, int l, int r, int x){
if(l != r){
int mid = l + r >> 1;
if(x <= mid){
mdf(p<<1, l, mid, x);
} else {
mdf(p<<1|1, mid+1, r, x);
}
t[p].id = geq(t[p<<1|1].id, t[p<<1].id) ? t[p<<1|1].id : t[p<<1].id;
t[p].cnt = calc(p<<1|1, mid+1, r, t[p<<1].id);
}
}
int main(){
scanf("%d%d", &n, &m);
build(1, 1, n);
for(int i = 1; i <= m; ++ i){
int x, y;
scanf("%d%d", &x, &y);
a[x] = y;
mdf(1, 1, n, x);
printf("%d\n", calc(1, 1, n, 0));
}
return 0;
}
24. CTT2012 - 序列染色
设 \(f_i\) 表示 \([1,i]\) 中不出现连续 \(k\) 个 \(B\) 的方案数,容易容斥解决(即在 \([i-k+1,i]\) 可以构成连续 \(k\) 个 \(B\) 时减去 \(f_{i-k-1}[s_{i-k}\neq \texttt B]\));于是就能求出 \(F_i\) 表示 \([1,i]\) 中第一段连续 \(k\) 个 \(B\) 为 \([i-k+1,i]\) 的方案数(即 \(f_{i-k-1}[s_{i-k}\neq\texttt B]\prod_{j\in(i-k,i]}[s_j\neq\texttt W]\))。
同理可求得 \(G_i\)。答案即为 \(\sum_{i<j}F_iG_j\prod_{k\in(i,j)}2^{[s_k=\texttt X]}\)。可以前缀和优化至线性。
点击查看代码
//qoj3680
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
const ll P = 1e9 + 7;
int n, k;
ll f[N], g[N], F[N], G[N];
char s[N];
#define cl(x) ((x) == 'X' ? 2 : 1)
int main(){
scanf("%d%d", &n, &k);
scanf("%s", s+1);
f[0] = g[n+1] = 1;
for(int i = 1, la = 0; i <= n; ++ i){
if(s[i] == 'W'){
la = i;
}
if(la >= i - k + 1){
f[i] = f[i-1] * cl(s[i]) % P;
} else {
int v = s[i-k] == 'B' ? 0 : i==k ? 1 : f[i-k-1];
f[i] = (P + f[i-1] * cl(s[i]) - v) % P;
}
}
for(int i = n; i >= 1; -- i){
f[i] = s[i] == 'B' ? 0 : f[i-1];
}
for(int i = 1, la = 0; i <= n; ++ i){
if(s[i] == 'W'){
la = i;
}
if(la < i - k + 1){
F[i] = f[i-k];
}
}
for(int i = n, la = n+1; i >= 1; -- i){
if(s[i] == 'B'){
la = i;
}
if(la <= i + k - 1){
g[i] = g[i+1] * cl(s[i]) % P;
} else {
int v = s[i+k] == 'W' ? 0 : n-i+1==k ? 1 : g[i+k+1];
g[i] = (P + g[i+1] * cl(s[i]) - v) % P;
}
}
for(int i = 1; i <= n; ++ i){
g[i] = s[i] == 'W' ? 0 : g[i+1];
}
for(int i = n, la = n+1; i >= 1; -- i){
if(s[i] == 'B'){
la = i;
}
if(la > i + k - 1){
G[i] = g[i+k];
}
}
ll ans = 0, sum = 0;
for(int i = 1; i <= n; ++ i){
sum = (sum * cl(s[i]) + F[i]) % P;
ans = (ans + sum * G[i+1]) % P;
}
printf("%lld\n", ans);
return 0;
}
25. CTT2012 - 模积和
考虑 \(n\bmod i=n-i\lfloor\dfrac ni\rfloor\),于是可以把所有东西拆出来然后数论分块求解。
拆完后答案是:
点击查看代码
//qoj3681
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 19940417, i2 = 9970209, i6 = 3323403;
ll n, m;
ll sm(ll x){
x %= P;
return x * (x+1) % P * i2 % P;
}
ll sm(ll l, ll r){
return (sm(r) - sm(l-1) + P) % P;
}
ll pw(ll x){
x %= P;
return x * (x+1) % P * (x+x+1) % P * i6 % P;
}
ll pw(ll l, ll r){
return (pw(r) - pw(l-1) + P) % P;
}
ll clc(ll x){
ll ans = 0;
for(ll l = 1, r; l <= x; l = r + 1){
r = x / (x / l);
ans = (ans + (x / l) % P * sm(l, r)) % P;
}
return ans;
}
ll calc(ll x, ll y){
ll ans = 0;
for(ll l = 1, r; l <= min(x, y); l = r + 1){
r = min(x / (x / l), y / (y / l));
ans += x * y % P * (r-l+1) % P;
ans -= y * (x / l) % P * sm(l, r) % P;
ans -= x * (y / l) % P * sm(l, r) % P;
ans += (x / l) * (y / l) % P * pw(l, r) % P;
ans = (ans % P + P) % P;
}
return ans;
}
int main(){
scanf("%lld%lld", &n, &m);
ll ans = 0;
ans += (n * n % P + P - clc(n)) * (m * m % P + P - clc(m)) % P;
ans -= calc(n, m);
printf("%lld\n", (ans % P + P) % P);
return 0;
}
26. NOI2023 - 桂花树
条件等价于 \(T'[1,n] =T\),\(T'[1,n+i]\) 的虚树上点编号 \(\in[1,i+k]\)。
于是考虑一个一个点加入,设现在树上共有 \(c\) 个点,有三种转移:
- 将 \(i\) 插入到一条边上/一个点上,方案数 \(2c-1\);
- 在一条边上插入一个编号 \(\in(i,i+k]\) 的点,并将 \(i\) 作为它的子节点,方案数 \(c-1\);
- 作为先前插入过的点,方案数 \(1\)。
维护 \(f_{i,S}\) 表示考虑了 \([n+1,n+i]\) 的点插入情况,\((n+i,n+i+k]\) 中点已插入集合为 \(S\) 的方案数。每次转移 \(2\) 时枚举 \(S\) 的补集即可。
这样是不重不漏的。
点击查看代码
//qoj6756
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3010, P = 1e9 + 7;
int c, T, n, m, k, f[1<<10], g[1<<10];
void upd(int &x, int y){
x += y;
if(x >= P){
x -= P;
}
}
int main(){
scanf("%d%d", &c, &T);
while(T--){
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i < n; ++ i){
scanf("%d", &c);
}
g[0] = 1;
for(int i = 1; i <= m; ++ i){
memset(f, 0, sizeof(f));
for(int s = 0; s < (1 << k); ++ s){
if(!g[s]){
continue;
}
int cnt = n + i - 1 + __builtin_popcount(s);
if(s & 1){
upd(f[s>>1], g[s]);
} else {
upd(f[s>>1], (ll)g[s] * (cnt + cnt - 1) % P);
for(int j = 0; j < k; ++ j){
if(!(((s >> 1) >> j) & 1)){
int t = (s >> 1) ^ (1 << j);
upd(f[t], (ll)g[s] * (cnt - 1) % P);
}
}
}
}
memcpy(g, f, sizeof(g));
}
printf("%lld\n", g[0]);
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
}
return 0;
}
27. NOI2023 - 方格染色
考虑把斜线与直线/斜线与斜线的交暴力算出来后直接扫描线即可。
如何算交:
- 对于截距相同的斜线,若有相交则合并(需多次判断);
- 判断斜线与每条直线的交点,去重后从答案中减去。
然后直接扫描线即可,需要离散化。
点击查看代码
//qoj6755
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n, m, Q, q;
struct rec{
int x, y, xx, yy;
} qr[N];
vector<rec> cur;
ll ans = 0;
int bx[N], by[N], bc;
struct scanline{
int l, r, val;
};
vector<scanline> g[N];
struct node{
int tag, mn, mv, vl;
} t[N*4];
void build(int p, int l, int r){
t[p].vl = t[p].mv = bx[r+1] - bx[l];
if(l != r){
int mid = l + r >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid+1, r);
}
}
void add(int p, int l, int r, int ql, int qr, int v){
if(qr < l || r < ql){
return;
} else if(ql <= l && r <= qr){
t[p].mn += v;
t[p].tag += v;
} else {
int mid = l + r >> 1;
t[p<<1].tag += t[p].tag;
t[p<<1].mn += t[p].tag;
t[p<<1|1].tag += t[p].tag;
t[p<<1|1].mn += t[p].tag;
t[p].tag = 0;
add(p<<1, l, mid, ql, qr, v);
add(p<<1|1, mid+1, r, ql, qr, v);
if(t[p<<1].mn == t[p<<1|1].mn){
t[p].mn = t[p<<1].mn;
t[p].mv = t[p<<1].mv + t[p<<1|1].mv;
} else if(t[p<<1].mn < t[p<<1|1].mn){
t[p].mn = t[p<<1].mn;
t[p].mv = t[p<<1].mv;
} else {
t[p].mn = t[p<<1|1].mn;
t[p].mv = t[p<<1|1].mv;
}
}
}
void smx(){
if(!q){
return;
}
for(int i = 1; i <= q; ++ i){
++ qr[i].xx;
++ qr[i].yy;
bx[bc+1] = qr[i].x;
bx[bc+2] = qr[i].xx;
by[bc+1] = qr[i].y;
by[bc+2] = qr[i].yy;
bc += 2;
}
sort(bx + 1, bx + bc + 1);
int mx = unique(bx + 1, bx + bc + 1) - bx - 1;
sort(by + 1, by + bc + 1);
int my = unique(by + 1, by + bc + 1) - by - 1;
for(int i = 1; i <= q; ++ i){
qr[i].x = lower_bound(bx + 1, bx + mx + 1, qr[i].x) - bx;
qr[i].xx = lower_bound(bx + 1, bx + mx + 1, qr[i].xx) - bx;
qr[i].y = lower_bound(by + 1, by + my + 1, qr[i].y) - by;
qr[i].yy = lower_bound(by + 1, by + my + 1, qr[i].yy) - by;
g[qr[i].y].push_back({qr[i].x, qr[i].xx, 1});
g[qr[i].yy].push_back({qr[i].x, qr[i].xx, -1});
}
build(1, 1, mx-1);
for(int i = 1; i < my; ++ i){
for(auto p : g[i]){
add(1, 1, mx-1, p.l, p.r-1, p.val);
}
ans += 1ll * (by[i+1] - by[i]) * (bx[mx] - bx[1] - (t[1].mn ? 0 : t[1].mv));
}
}
int main(){
scanf("%d", &n);
scanf("%d%d%d", &n, &m, &Q);
for(int i = 1; i <= Q; ++ i){
int op, x, y, xx, yy;
scanf("%d%d%d%d%d", &op, &x, &y, &xx, &yy);
if(x > xx){
swap(x, xx);
}
if(y > yy){
swap(y, yy);
}
if(op != 3){
qr[++q] = {x, y, xx, yy};
} else {
cur.push_back({x, y, xx, yy});
}
}
int k = cur.size();
for(int p = 0; p < k; ++ p){
for(int i = 0; i < k; ++ i){
for(int j = 0; j < k; ++ j){
if(cur[i].x == -1 || cur[j].x == -1 || i == j){
continue;
}
if(cur[i].y - cur[i].x != cur[j].y - cur[j].x){
continue;
}
if(cur[i].x > cur[j].xx || cur[j].x > cur[i].xx){
continue;
}
cur[i].x = min(cur[i].x, cur[j].x);
cur[i].y = min(cur[i].y, cur[j].y);
cur[i].xx = max(cur[i].xx, cur[j].xx);
cur[i].yy = max(cur[i].yy, cur[j].yy);
cur[j].x = -1;
}
}
}
for(int i = 0; i < k; ++ i){
if(cur[i].x == -1){
continue;
}
map<int, int> mp;
mp.clear();
int cnt = 0, b = cur[i].y - cur[i].x;
for(int j = 1; j <= q; ++ j){
if(qr[j].x == qr[j].xx){
int x = qr[j].x, y = qr[j].x + b;
if(x >= cur[i].x && x <= cur[i].xx &&
y >= qr[j].y && y <= qr[j].yy && !mp[x]){
mp[x] = 1;
++ cnt;
}
} else {
int x = qr[j].y - b, y = qr[j].y;
if(y >= cur[i].y && y <= cur[i].yy &&
x >= qr[j].x && x <= qr[j].xx && !mp[x]){
mp[x] = 1;
++ cnt;
}
}
}
ans += cur[i].xx - cur[i].x + 1 - cnt;
}
smx();
printf("%lld\n", ans);
return 0;
}
28. APIO2016 - Boat
考虑一个 \(O(n^2V)\) 的做法:设 \(f_{i,j}\) 表示到第 \(i\) 个点,最高选中为 \(j\) 的方案数,使用前缀和优化即可。
发现 \(V=10^9\),而状态中很难去掉这个 \(V\),考虑离散化,将一段值域合并为一个状态。
考虑设 \(calc(l,r,k)\) 表示 \([l,r]\) 区间内选择值域在第 \(k\) 个区间内,\(r\) 一定要选的方案数。设 \(n=\sum_{i\in[l,r]}[k\in[a_i,b_i]]\)(离散化后),易得:
于是就可以利用这个来 dp。设 \(f_{i,j}\) 表示前 \(i\) 个,第 \(i\) 个选了值域在第 \(j\) 个区间的方案数。\(g_{i,j}=\sum_{k=0}^j f_{i,k}\)。那么有转移:
本质不同的 \(calc\) 仅有 \(O(n^2)\) 个,可以预处理。总复杂度 \(O(n^3)\)。
点击查看代码
//qoj2
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 1e9 + 7;
const int N = 510, M = 1010;
int n, a[N], b[N], pl[M];
ll f[N][M], g[N][M], h[N][M];
ll fac[N], inv[N];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
scanf("%d", &n);
fac[0] = 1;
for(int i = 1; i <= n; ++ i){
fac[i] = fac[i-1] * i % P;
scanf("%d%d", &a[i], &b[i]);
pl[i] = a[i];
pl[i+n] = b[i] + 1;
}
inv[n] = qp(fac[n], P-2);
for(int i = n-1; i >= 0; -- i){
inv[i] = inv[i+1] * (i+1) % P;
}
sort(pl + 1, pl + n + n + 1);
int m = unique(pl + 1, pl + n + n + 1) - pl - 1;
for(int i = 1; i <= n; ++ i){
a[i] = lower_bound(pl + 1, pl + m + 1, a[i]) - pl;
b[i] = lower_bound(pl + 1, pl + m + 1, b[i] + 1) - pl - 1;
}
f[0][0] = 1;
for(int j = 0; j < m; ++ j){
g[0][j] = 1;
}
for(int j = 1; j < m; ++ j){
h[0][j] = 1;
ll val = 1;
for(int i = 1; i <= n; ++ i){
val = val * (pl[j+1] - pl[j] + i - 1) % P;
h[i][j] = val * inv[i] % P;
}
}
for(int i = 1; i <= n; ++ i){
for(int j = 1; j < m; ++ j){
if(j < a[i] || j > b[i]){
continue;
}
int cnt = 0;
for(int k = i; k >= 1; -- k){
if(a[k] <= j && j <= b[k]){
++ cnt;
}
f[i][j] = (f[i][j] + g[k-1][j-1] * h[cnt][j]) % P;
}
}
for(int j = 1; j <= m; ++ j){
g[i][j] = (g[i][j-1] + f[i][j]) % P;
}
}
ll ans = 0;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j < m; ++ j){
ans = (ans + f[i][j]) % P;
}
}
printf("%lld\n", ans);
return 0;
}
29. APIO2016 - Fireworks
设 \(F_i(x)\) 表示令 \(i\) 的子树中导火索长为 \(x\) 的最小代价。容易发现这是一个下凸函数。考虑 \(F_i(x)\) 转移到父节点,设 \([L,R]\) 表示 \(F_i(x)\) 斜率为 \(0\) 的一段的横坐标,则有:
原因是 \(F_{fa}(x)\) 只能从 \(F_i([0,x])\) 转移过来,而斜率一定是整数,故转移 \(1\) 从相同横坐标的地方转移,转移 \(234\) 从距离 \(x-l\) 最近的最低点转移。
那么转移在图像上形如:
- 将 \(>R\) 的地方全部换为斜率为 \(=1\) 的直线;
- 将 \([L,R]\) 斜率为 \(0\) 的直线向右平移 \(l\);
- 在 \([L,L+l]\) 之间插入斜率为 \(-1\) 的直线。
考虑维护每个转折点的横坐标;若相邻斜率差 \(>1\) 可以看做同一横坐标出现多次。那么转移就可以通过以下方式维护:
- 弹出最大的 \(ind_i-1\) 个横坐标(其中 \(ind_i\) 表示 \(i\) 的儿子个数);
- 弹出最大的 \(2\) 个横坐标,\(+l\) 后插入回去。
然后两个函数合并则直接将维护横坐标的堆合并即可。使用左偏树维护。
最后求答案时,由于 \(F_1(0)=\sum C_i\),所以可以直接求解。一个好写的做法是弹出目前最后一个横坐标(表示斜率由 \(0\to -1\)),然后将 \(F_1(0)\) 减去堆中所有横坐标和。容易发现这是正确的。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 6e5 + 10;
int n, m, f[N], v[N], ind[N];
ll sum;
int rt[N], top;
struct node{
int ls, rs, ds;
ll x;
} t[N];
int mg(int x, int y){
if(!x || !y){
return x + y;
}
if(t[x].x < t[y].x || (t[x].x == t[y].x && x < y)){
swap(x, y);
}
t[x].rs = mg(t[x].rs, y);
if(t[t[x].ls].ds < t[t[x].rs].ds){
swap(t[x].ls, t[x].rs);
}
t[x].ds = t[t[x].rs].ds + 1;
return x;
}
int pop(int x){
return mg(t[x].ls, t[x].rs);
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 2; i <= n + m; ++ i){
scanf("%d%d", &f[i], &v[i]);
++ ind[f[i]];
sum += v[i];
}
for(int i = n + m; i > 1; -- i){
ll x = 0, y = 0;
if(i <= n){
for(int j = 1; j < ind[i]; ++ j){
rt[i] = pop(rt[i]);
}
y = t[rt[i]].x;
rt[i] = pop(rt[i]);
x = t[rt[i]].x;
rt[i] = pop(rt[i]);
}
t[++top].x = x + v[i];
t[++top].x = y + v[i];
rt[i] = mg(rt[i], mg(top-1, top));
rt[f[i]] = mg(rt[f[i]], rt[i]);
}
for(int j = 1; j < ind[1]; ++ j){
rt[1] = pop(rt[1]);
}
rt[1] = pop(rt[1]);
while(rt[1]){
sum -= t[rt[1]].x;
rt[1] = pop(rt[1]);
}
printf("%lld\n", sum);
return 0;
}
30. APIO2016 - Gap
Sub1:
询问 \((-\inf,\inf)\) 即可得到 \(a_0,a_{n-1}\);询问 \((a_0+1,a_{n-1}-1)\) 即可得到 \(a_1,a_{n-2}\),以此类推得到整个 \(a\) 数组后扫描一遍求最大差分值即可。
Sub2:
首先询问 \((-\inf,\inf)\) 得到 \(a_0,a_{n-1}\)。那么答案一定 \(\geq \lceil\dfrac{a_{n-1}-a_0}{n-1}\rceil\)。设这个值为 \(k\),对于 \((a_0,a_{n-1})\) 中每相邻 \(k\) 个数查询一次,存储查询到的 \(a\) 集合,那么构成答案的 \(a_i,a_{i+1}\) 一定都在查询到的 \(a\) 集合中。
点击查看代码
//qoj4
#include <bits/stdc++.h>
using namespace std;
#include "gap.h"
long long findGap(int T, int n){
static long long a[200010];
memset(a, 0, sizeof(a));
if(T == 1){
a[0] = -3, a[n+1] = 2e18;
for(int i = 1, j = n; i <= j; ++ i, -- j){
MinMax(a[i-1] + 1, a[j+1] - 1, &a[i], &a[j]);
}
long long mx = 0;
for(int i = 1; i < n; ++ i){
mx = max(mx, a[i+1] - a[i]);
}
return mx;
} else {
long long mn, mx;
MinMax(-3, 2e18, &mn, &mx);
long long k = (mx - mn - 1) / (n - 1) + 1;
int cnt = 0;
if(mx - mn <= 1){
return mx - mn;
}
a[++cnt] = mn;
a[++cnt] = mx;
++ mn;
-- mx;
for(long long i = mn; i <= mx; i += k){
long long tx, ty;
MinMax(i, i+k-1, &tx, &ty);
a[++cnt] = tx;
a[++cnt] = ty;
}
sort(a + 1, a + cnt + 1);
mx = 0;
for(int i = 1; i < cnt; ++ i){
mx = max(mx, a[i+1] - a[i]);
}
return mx;
}
}
31. CF713C - Sonya and Problem Wihtout a Legend
首先令 \(a_i\to a_i-i\),然后目标转化为序列单调不降。
维护函数 \(f_i(x)\) 表示前 \(i\) 个数单调不降,最后一个数为 \(x\) 的最小代价,容易发现每次转移相当于加上一个斜率先 \(-1\) 后 \(1\) 的函数,再将末尾斜率 \(>0\) 的部分变为 \(0\)。所以我们可以使用小根堆维护拐点。每次插入两次 \(a_i-i\),删除一次最大值(插入后末尾斜率 \(>0\) 的部分仅有斜率 \(=1\) 一段)。最后统计答案时,使用经典套路 \(f(0)\) 减去堆中所有值即可。
点击查看代码
// Problem: Sonya and Problem Wihtout a Legend
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF713C
// Memory Limit: 250 MB
// Time Limit: 5000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, a;
ll ans = 0;
priority_queue<int> q;
int main(){
int T = 1;
while(T--){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a);
a -= i;
ans += a;
q.push(a);
q.push(a);
q.pop();
}
while(!q.empty()){
ans -= q.top();
q.pop();
}
printf("%lld\n", ans);
}
return 0;
}