ABC205~209
abc205
D
二分 + 前缀和
我们对每两个相邻 \(a\) 数组元素的距离做前缀和,我们通过二分即可以得到答案的区间,使用我们已经预处理过的前缀和数组即可得到前缀 \(a\) 数组元素的数量,让它与 \(k\) 相加即可。
inline void solve()
{
int n,m; fin >> n >> m;
std::vector<int> s(n+1),a(n+1);
for(int i = 1;i <= n;i++)
{
fin >> a[i];
s[i] = s[i-1] + a[i] - a[i-1] - 1;
}
while(m--)
{
int k; fin >> k;
int x = std::lower_bound(s.begin()+1,s.end(),k)-s.begin()-1;
fout << k+a[x]-s[x] << '\n';
}
}
E
组合数学 + 卡特兰数
一个卡特兰数的变体,考虑对点 \((0,0)\) 沿直线 $ y = x+k+1$ 镜像翻转。翻转后的起点要到达终点必须要穿过该直线,拿总方案数减去反转后的方案数即可。
int qp(int a,int b){int res=1;for(;b;b>>=1,a=a*a%M)if(b&1)res=res*a%M;return res%M;}
int fac(int x){int res = 1;for(int i = 2;i <= x;i++)res = (res*i)%M;return res%M;}
int C(int x,int y)
{
if(y < 0 || y > x) return 0;
return (((fac(x)*qp(fac(x-y),M-2))%M)*qp(fac(y),M-2))%M;
}
inline void solve()
{
int n,m,k; fin >> n >> m >> k;
if(n > m + k)
{
fout << 0 << '\n';
return ;
}
fout << (C(n+m,m) - C(n+m,m+k+1)+M)%M <<'\n';
}
F
图论建模 + 网络流
神仙网络流建模题,
看到此类的问题,我们可以想到使用最大流来进行约束。
考虑对每一行都建立源点,对每一列都建立汇点,此时我们再建立一个超级源点和一个超级汇点来连接所有源点以及所有汇点,并将这些连接超级源/汇点以及源/汇点之间的边容量设为 \(1\) ,表示我们每一行和每一列都只能放一个点。
我们来考虑放棋子,每一个棋子的容量显然是 \(1\) ,并且该棋子能够让从行 A ~ C 的源点流入,能够从列 B ~ D 的汇点流出。
因为我们很难对点去设置容量,所有考虑拆点,将一枚棋子分成入点 \(u\) 以及出点 \(v\) ,连接点 \(u,v\) 并设置边容量为 \(1\)。
最后从超级源点到超级汇点跑一遍最大流就可以了。
#define int long long
const int N = 200100;
#define inf 0x3f3f3f3f
struct E {int to, w,nxt;} E[N<<1];
int tot = 1, H[N];
inline void Add(int u, int v, int c)
{
E[++tot] = {v,c,H[u]};
H[u] = tot;
}
inline void add(int u,int v,int c){Add(u,v,c),Add(v,u,0);}
int cur[N],d[N],vis[N],S,T;
int n,m,k;
bool bfs()
{
memset(d,-1,sizeof(d));
std::queue<int> q;
cur[S] = H[S];
q.push(S);d[S]=0;
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = H[u];~i;i = E[i].nxt)
{
int v = E[i].to ,w = E[i].w;
if(~d[v] || !w) continue;
d[v] = d[u]+1;
cur[v] = H[v];
if(v == T) return 1;
q.push(v);
}
}
return 0;
}
int dfs(int s,int lim)
{
if(s == T) return lim;
int flow = 0;
for(int &i = cur[s];~i && flow < lim; i = E[i].nxt)
{
int v = E[i].to;
if(d[v] != d[s]+1||!E[i].w) continue;
int t = dfs(v,std::min(E[i].w,lim-flow));
if(!t) d[v]=-1;
E[i].w-=t; E[i^1].w+=t;
flow+=t;
}
return flow;
}
int dinic()
{
int res = 0;
while (bfs()) res += dfs(S, inf);
return res;
}
void build()
{
S = 0, T = 2*k+n+m+1;
memset(H,-1,sizeof(H));
for(int i = 1;i <= n;i++) add(S,i,1);
for(int i = 1;i <= m;i++) add(n+2*k+i,T,1);
for(int i = 1;i <= k;i++)
{
int a,b,c,d;
fin >> a >> b >> c >> d;
for(int j = a;j <= c;j++)
add(j,n+i,1);
for(int j = b;j <= d;j++)
add(n+k+i,n+2*k+j,1);
add(n+i,n+k+i,1);
}
}
inline void solve()
{
fin >> n >> m >> k;
build(); // 建图
fout << dinic() << '\n'; // 跑最大流
}
abc206
D
并查集
比较明显的一道并查集的题,由于我们需要去动态维护每个元素做了变更后的新值,用一般的 map 无法进行动态维护,所以考虑并查集。
使用两个指针 \(i,j\) ,让 \(i\) 从左往右走,让 \(j\) 从右往左走,若 \(i,j\) 两个指针指的元素不同,则让全部的 \(i\) 指的元素更改为 \(j\) 指的元素。
#define int long long
const int N = 1e6+10;
int fa[N],a[N],b[N],tot;
std::map<int,int> p;
int n;
int find(int x)
{
if(fa[x] == x)return fa[x];
return fa[x] = find(fa[x]);
}
inline void solve()
{
fin >> n;
std::vector<int> a(n+1);
for(int i = 1;i <= n;i++) fin >> a[i];
for(int i = 1;i <= n;i++)
{
if(!p[a[i]])
{
b[++tot] = a[i];
p[a[i]] = i;
}
fa[i] = p[a[i]];
}
int j = n+1,ans = 0;;
for(int i = 1;i <= n/2;i++)
{
j--;
if(find(i) != find(j))
{
fa[find(i)] = find(j);
ans++;
}
}
fout << ans << '\n';
}
E
莫比乌斯反演 + 容斥原理
直接求这个式子太难了,考虑求补集,分别是 \(gcd(i,j) = 1\) 或 \(i,j\) 互为倍数。显然,这两种情况很难同时出现,当且仅当 \(i\) 或 \(j\) 为 \(1\) 时才会出现,所以我们枚举倍数时直接从 \(2\) 开始就好。
对于 \(gcd(i,j) = 1\) 的情况,由于这是一个元函数,所以我们可以对它进行莫比乌斯反演,反演完后,我们可以直接在线性复杂度内计算这个和式(太经典了,这里不细讲)。
当然,我们很显然可以对莫比乌斯函数使用杜教筛来处理,然后对本该线性复杂度内计算的式子使用整除分块,这样的话我们的复杂度就做到了 \(O(n^{2/3})\) 。
对于 \(i,j\) 互为倍数的情况,跑两遍整除分块,相加,把重复计算的值 \(n-1\) 减去即可。
#define int long long
const int N = 1e4+10;
int ans,smu[N];
int n,tot,prm[N],mu[N];
bool vis[N];
std::map<int,int> mem;
int l,r;
// 杜教筛
int MU(int x)
{
if(x < N) return smu[x];
if(mem[x]) return mem[x];
int res = 1;
for(int l = 2,r;l <= x;l = r+1)
{
r = x/(x/l);
res -= MU(x/l) * (r-l+1);
}
return mem[x] = res;
}
// 欧拉筛预处理莫比乌斯函数
void sieve(int x)
{
vis[1] = mu[1] = smu[1] = 1;
for(int i = 2;i < x;i++)
{
if(!vis[i]) prm[++prm[0]] = i,mu[i] = -1;
for(int j = 1;j <= prm[0] && i * prm[j] < x;j++)
{
vis[i*prm[j]] = 1;
if(i % prm[j] == 0)
{
mu[i*prm[j]] = 0;
break;
}
mu[i*prm[j]] = -mu[i];
}
smu[i] = smu[i-1] + mu[i];
}
}
int f(int x,int y)
{
if(!x||!y) return 0;
int n = std::min(x,y);
int m = std::max(x,y);
int res = 0;
for(int l = 2,r;l <= n;l = r + 1)
{
r = std::min(n,m /(m/l));
res += (r-l+1) * (m/l);
}
int t = 0;
for(int l = 2,r;l <= n;l = r + 1)
{
r = n/(n/l);
t += (r-l+1)*(n/l);
}
res += t-n+1;
for(int l = 1,r;l <= n;l = r+1)
{
r = std::min(n / (n / l),m / (m / l));
res += (MU(r) - MU(l-1)) * (n/l) * (m/l);
}
return res;
}
F
博弈论 + SG函数
我们发现这是一个公平组合游戏,使用 SG 函数进行求解。
根据公平组合游戏的定理,我们用子区间将区间分开后,所产生的 SG 函数值就是分开的两个区间的 SG 函数值的异或和。
最后得到 \(SG(1,m)\) 的值,必输态下为 \(0\) ,反之为必胜态,判断即可。
const int N = 105;
struct node{int l,r;}s[N];
int sg[N][N];
inline void solve()
{
int n; fin >> n;
int m = 0;
for(int i = 1;i <= n;i++)
{
fin >> s[i].l >> s[i].r;
m = std::max(m,s[i].r);
}
std::sort(s+1,s+1+n,[](node a,node b)
{
if(a.l == b.l) return a.r < b.r;
return a.l < b.l;
});
for(int len = 1;len <= m;len++)
for(int i = 1;i + len <= m;i++)
{
int j = i+len;
std::vector<int> vis(n+1);
for(int k = 1;k <= n;k++)
if(s[k].l >= i && s[k].r <= j)
vis[sg[i][s[k].l]^sg[s[k].r][j]] = 1;
for(int k = 0;;k++)
if(!vis[k])
{
sg[i][j] = k;
break;
}
}
if(sg[1][m])
fout << "Alice\n";
else
fout << "Bob\n";
}
abc207
E
DP + 前缀和
一道不错的 DP 题,我们有暴力的 \(O(n^3)\) 的转移方程,也就是让 \(f(i,j)\) 表示对于前 \(i\) 个元素我们将其分成 \(j\) 份的方案,我们可以每次枚举分界点然后使用前缀和快速计算是否满足条件。
我们需要对其进行优化,
设辅助数组 $g_{i,j} = \sum f_{k,i}[s_k \bmod (i+1) = j] $ 。
数组 \(g\) 可以在转移时累加。
#define int long long
const int N = 3010;
const int M = 1e9+7;
int g[N][N],f[N][N];
inline void solve()
{
int n; fin >> n;
std::vector<int> a(n+1),s(n+1);
for(int i = 1;i <= n;i++) fin >> a[i];
for(int i = 1;i <= n;i++) s[i] = s[i-1] + a[i];
f[0][0] = g[0][0] = 1;
for(int i = 1;i <= n;i++)
for(int j = n;j >= 1;j--)
{
f[i][j] = g[j-1][s[i]%j];
g[j][s[i]%(j+1)] += f[i][j];
g[j][s[i]%(j+1)] %= M;
}
int ans = 0;
for(int i = 1;i <= n;i++)
ans = (ans + f[n][i]) % M;
fout << ans << '\n';
}
F
树上背包 + 分类讨论
细节很多的树上背包,设 $f_{u,i,x,y} $ 表示在 \(u\) 的子树内,有 \(i\) 个节点被控制,\(x\) 为节点 \(u\) 是否拥有警卫,\(y\) 为节点 \(u\) 是否被控制。
对于三种不同的情况分类讨论即可。
#define int long long
const int N = 2e3+10;
const int M = 1e9+7;
int n,f[N][N][2][2],tmp[N][2][2],siz[N];
std::vector<int> E[N];
void dfs(int u,int fa)
{
f[u][0][0][0] = f[u][1][1][1] = 1;
siz[u] = 1;
for(int v : E[u])
{
if(v == fa) continue;
dfs(v,u);
for(int i = 0;i <= siz[u] + siz[v];i++)
tmp[i][0][0] = tmp[i][0][1] = tmp[i][1][1] = 0;
for(int i = siz[u];i >= 0;i--)
for(int j = siz[v];j >= 0;j--)
{
tmp[i+j][0][0] += f[u][i][0][0] * (f[v][j][0][0] + f[v][j][0][1]);
tmp[i+j][0][0] %= M;
if(i >= 1)
{
tmp[i+j][0][1] += f[u][i-1][0][0] * f[v][j][1][1] + f[u][i][0][1] * (f[v][j][0][0] + f[v][j][0][1] + f[v][j][1][1]);
tmp[i+j][0][1] %= M;
tmp[i+j][1][1] += f[u][i][1][1] * ((j >= 1 ? f[v][j-1][0][0] : 0) + f[v][j][0][1] + f[v][j][1][1]);
tmp[i+j][1][1] %= M;
}
}
for(int i = 0;i <= siz[u] + siz[v];i++)
{
f[u][i][0][0] = tmp[i][0][0];
f[u][i][0][1] = tmp[i][0][1];
f[u][i][1][1] = tmp[i][1][1];
}
siz[u] += siz[v];
}
}
inline void solve()
{
fin >> n;
for(int i = 1;i < n;i++)
{
int u, v; fin >> u >> v;
E[u].emplace_back(v);
E[v].emplace_back(u);
}
dfs(1,0);
for(int k = 0;k <= n;k++)
fout << (f[1][k][0][0] + f[1][k][0][1] + f[1][k][1][1])%M << '\n';
}
abc208
D
DP + 图论 + 最短路
想一下 Floyd 算法的本质,我们将第一维的 k 扩展出来,就能够表示只在 \(1 - k\) 的点进行松弛得出来的最短路,最后相加即可。
#define int long long
#define inf 0x3f3f3f3f
const int N = 410;
int dis[N][N][N],ans,n,m;
void floyd()
{
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
dis[k][i][j] = std::min(dis[k-1][i][j],dis[k-1][i][k]+dis[k-1][k][j]);
}
inline void solve()
{
memset(dis,0x3f,sizeof(dis));
fin >> n >> m;
for(int i = 1;i <= m;i++)
{
int u,v,w; fin >> u >> v >> w;
dis[0][u][v] = w;
}
for(int i = 0;i <= n;i++)
for(int j = 1;j <= n;j++)
dis[i][j][j] = 0;
floyd();
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
ans += dis[k][i][j] * (dis[k][i][j] < inf);
fout << ans << '\n';
}
F
拉插 + 组合数学
根据递归式的定义,我们发现答案为 \(1^k,2^k,3^k,⋯,n^k\) 的 \(m\) 阶前缀和的第 \(n\) 项值。
因为 \(n\) 实在太大了,所以我们不能直接进行卷积,考虑别的方法。
考虑一下 \(i^k\) 对答案的贡献次数,每次做前缀和都会扩展新的可供转移的一行,贡献的次数即为 \((1,0) \to (m,n-i)\) 的路径数,即 \(C(n-i+m-1,m-1)\)。
展开一下组合数,发现我们和式里面要求的东西本质上是一个关于 \(i\) 的 \((m+k-1)\) 次多项式。
那我们取出前 \(m+k-1\) 项,算出其作 \(m\) 阶前缀和的答案,拉格朗日插值即可。
const int M = 1e9+7;
const int N = 3e6+10;
int qp(int a,int b){int res=1;for(;b;b>>=1,a=a*a%M)if(b&1)res=res*a%M;return res;}
int l,n,m,k,a[N],fac[N],inv[N],pre[N],suf[N],ans;
inline void init()
{
inv[0] = fac[0] = 1; for(int i = 1; i <= l; i++) fac[i] = fac[i-1] * i % M;
inv[l] = qp(fac[l], M-2); n %= M;
for(int i = l-1; i; i--) inv[i] = inv[i+1] * (i+1) % M;
pre[0] = 1; for(int i = 1; i <= l; i++) pre[i] = pre[i-1] * (n - i) % M;
suf[l+1] = 1; for(int i = l; i; i--) suf[i] = suf[i+1] * (n - i) % M;
}
void lagrange()
{
init();
for(int i = 1;i <= l;i++)
ans=(ans+(a[i]*pre[i-1]%M*suf[i+1]%M*inv[i-1]%M*inv[l-i]%M*(((l-i)&1)?-1:1)+M)%M)%M;
}
inline void solve()
{
n = read(), m = read(), k = read();
l = m+k+1;
if(!n) {return void(fout<<"0\n");}
for(int i = 1;i <= l;i++) a[i] = qp(i,k);
for(int i = 1;i <= m;i++)
for(int j = 1;j <= l;j++)
a[j] = (a[j]+a[j-1])%M;
lagrange();
write(ans);
}
abc209
D
DFS
目前做过的最简单的 D 题,预处理一遍每个节点的深度,若两个节点深度之和为偶数,则最短路径为偶数,因为最短路径要消去的 \(dep[lca]\times2\) 一定是偶数。偶数减偶数一定是偶数,奇数减偶数一定是奇数。
const int N = 1e5+10;
std::vector<int> E[N];
int u,v;
int dep[N];
void dfs(int u,int fa)
{
dep[u] = dep[fa] + 1;
for(int v : E[u])
{
if(v == fa) continue;
dfs(v,u);
}
}
inline void solve()
{
int n,q; fin >> n >> q;
for(int i = 1;i < n;i++)
{
fin >> u >> v;
E[u].emplace_back(v);
E[v].emplace_back(u);
}
dfs(1,0);
while(q--)
{
fin >> u >> v;
if((dep[u] + dep[v]) % 2)
fout << "Road\n";
else
fout << "Town\n";
}
}
E
字符串哈希 + DFS
这题感觉 2200* 还是评高了,
用每个单词的前三个字符向后三个字符用哈希连边。
显然,若一个点的出度为 \(0\) ,则该点为 \(N\) 态。
若一个点连向的点里有 \(N\) 态,则该点为 \(P\) 态。
若一个点连向的点都是 \(P\) 态,则该点为 \(N\) 态。
若当前点访问到了第二次,则该点为平局点。
(和常规的转移刚好相反)。
#define int long long
#define pii std::pair<int,int>
const int N = 2e5+10;
std::vector<int> E[N];
std::string s[N];
int ans[N],f[N];
std::map<pii,bool> mp;
int g(char a)
{
if(a>='a') return a-'a'+27;
return a-'A';
}
int hs(std::string s){return g(s[0])*53*53+g(s[1])*53+g(s[2]);}
int dfs(int u)
{
if(E[u].empty()) return 2;
if(ans[u]) return ans[u];
if(f[u] == 2) return ans[u]=3;
f[u]++;
int res = 2;
for(int v : E[u])
{
if(f[v]) continue;
int w = dfs(v);
if(w == 3) res = 3;
if(w == 2) return ans[u] = 1;
}
for(int v : E[u])
{
if(!f[v]) continue;
int w = dfs(v);
if(w == 3) res = 3;
if(w == 2) return ans[u] = 1;
}
return ans[u] = res;
}
inline void solve()
{
int n; fin >> n;
for(int i = 1;i <= n;i++)
{
fin >> s[i];
std::string x = "",y = "";
x += s[i][0];x += s[i][1];x += s[i][2];
y += s[i][s[i].size()-3];
y += s[i][s[i].size()-2];
y += s[i][s[i].size()-1];
int a = hs(x),b = hs(y);
if(mp[{a,b}]) continue;
mp[{a,b}] = 1;
E[a].emplace_back(b);
}
for(int i = 1;i <= n;i++)
{
std::string y = "";
y += s[i][s[i].size()-3];
y += s[i][s[i].size()-2];
y += s[i][s[i].size()-1];
int ans = dfs(hs(y));
if(ans == 2) fout << "Takahashi\n";
if(ans == 1) fout << "Aoki\n";
if(ans == 3) fout << "Draw\n";
}
}
F
动态规划 + 前缀和
做一些转化,
发现对于两个相邻的数,选更大的一定更优,
我们把数与数之间的关系放到一个新序列上,设状态 \(f(i,j)\) 表示前 \(i\) 个数放了 \(i\) 的排列,且末尾为 \(j\) 的方案数。
得到转移方程
$ f_{i,j} = \sum^{j-1}{k=1} f (s_i = '<')$
$ f_{i,j} = \sum^{i-1}{k=j} f (s_i = '>')$
做一下前缀和优化即可。
#define int long long
const int N = 4e3+10;
const int M = 1e9+7;
int f[N][N],s[N][N];
inline void solve()
{
int n; fin >> n;
std::vector<int> a(n+1);
for(int i = 1;i <= n;i++) fin >> a[i];
f[1][1] = 1;
for(int i = 1;i <= n;i++)s[1][i] = 1;
for(int i = 2;i <= n;i++)
for(int j = 1;j <= i;j++)
{
if(a[i] < a[i-1]) f[i][j] = s[i-1][j-1];
if(a[i] > a[i-1]) f[i][j] = (s[i-1][i-1] - s[i-1][j-1] + M) %M;
if(a[i] == a[i-1]) f[i][j] = s[i-1][i-1];
s[i][j] = (s[i][j-1] + f[i][j])%M;
}
fout << s[n][n] <<'\n';
}

浙公网安备 33010602011771号