杂题 I
随便记一些最近做的题目吧。
网格图最小生成树
题目大意
给定一个 \(n\) 行 \(m\) 列的网格图(\(1\le n,m\le 10^6\)),共有 \(n\times m\) 个点,网格图共有两种边:
对于 \(x\ne n\) 的点,存在一条连接 \((x,y),(x+1,y)\) 的边,边权为 \(A_x+B_y\)。
对于 \(y\ne m\) 的边,存在一条连接 \((x,y),(x,y+1)\) 的边,边权为 \(C_x+D_y\)。
其中 \(A,B,C,D\) 均为单调不减的权值数组,其中 \(0\le A_i,B_i,C_i,D_i\le 10^8\)。
求出该网格图的最小生成树。
思路分析
看似是图论题,但 \(O(nm)\) 的复杂度无法承受,所以必须另想它法。
结论 \(1\):对于点 \((x,y)\),其一定存在一条 \((x-1,y)\to (x,y)\) 的边或 \((x,y-1)\to (x,y)\) 的边。
证明:假设对于点 \((x,y)\),其仅仅存在一条 \((x,y)\to (x+1,y)\) 的边与之相连。
这条边的权值为 \(A_x+B_y\),我们将之删掉,此时的 \((x,y)\) 为一个孤立点,我们再连接 \((x-1,y)\to (x,y)\),这条边的权值为 \(A_{x-1}+B_y\)。
因为 \(A_x\ge A_{x-1}\),所以 \(A_x+B_y\ge A_{x-1}+B_y\),我们得到了一个更小的方案,其他情况以此类推,故原结论成立
结论 \(2\):对于点 \((x,y)\),其仅仅存在一条 \((x-1,y)\to (x,y)\) 的边或 \((x,y-1)\to (x,y)\) 的边。
假如说存在 \((x-1,y)\to (x,y),(x,y-1)\to (x,y)\),因为此时是一颗树,所以 \((x-1,y)\to (x-1,y-1)\) 或 \((x-1,y-1)\to (x,y-1)\) 这两条边中至少有一条不存在。我们把前面提到的 \((x-1,y)\to (x,y),(x,y-1)\to (x,y)\) 平移过去,就会得到一个权值和更小的方案。
根据上面两个结论,最后的答案就是:
\]
前面两坨是最上边和最左边的点的权值,因为结论 \(2\),他们所连的边是一定的,主要对最后的式子进行处理。
我们先让每个点 \((x,y)\) 都选择方案 \(A_{x-1}+B_y\),统计完答案后,如果满足 \(C_x+D_{y-1}<A_{x-1}+B_y\),就令前面统计的答案加上 \(C_x+D_{y-1}-A_{x-1}-B_y\)。
观察式子
\]
移项得:
\]
令 \(S_x=C_x-A_{x-1},T_y=B_y-D_{y-1}\)。显然,当 \(S_x<T_y\) 时,此时可以将方案替换,我们将两项作差得:
\]
将式子转换一下为:
\]
预处理出 \(T_y\) 的后缀和,枚举 \(S_x\),二分查找第一个 \(T_y>S_x\) 的位置 \(y\),然后直接统计后缀和,时间复杂度 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=1e6+10;
typedef long long ll;
ll a[N],b[N],c[N],d[N],n,m,ans=0,S[N],T[N],sumT[N];
signed main(){
scanf("%lld %lld",&n,&m);
for(int i=1;i<n;i++)scanf("%lld",a+i);
for(int i=1;i<=m;i++)scanf("%lld",b+i);
for(int i=1;i<=n;i++)scanf("%lld",c+i);
for(int i=1;i<m;i++)scanf("%lld",d+i);
for(int i=1;i<=m-1;i++)ans+=c[1]+d[i];
for(int i=1;i<=n-1;i++)ans+=a[i]+b[1];
for(int i=1;i<=n-1;i++)ans+=a[i]*(m-1);
for(int i=2;i<=m;i++)ans+=b[i]*(n-1);
for(int i=2;i<=n;i++)S[i]=c[i]-a[i-1];
for(int i=2;i<=m;i++)T[i]=b[i]-d[i-1];
sort(T+2,T+1+m);
for(int i=m;i>=2;i--)sumT[i]=sumT[i+1]+T[i];
for(int i=2;i<=n;i++){
if(S[i]>=T[m])continue;
int pos=upper_bound(T+2,T+1+m,S[i])-T;
ans+=(m-pos+1)*S[i]-sumT[pos];
}
printf("%lld\n",ans);
return 0;
}
密码锁
题目大意
定义“合法字符串”为满足字符单调递增的字符串,定义一次变换操作为将某个字符变为变为其相邻的字符(如 \(\texttt{b}\) 可以变为 \(\texttt{a}\),\(\texttt{c}\),但 \(\texttt{a}\) 不可以变成 \(\texttt{z}\))。
给定一个长为 \(n\) 字符串 \(S\),首先回答将给定字符串变为“合法字符串”的最少变换次数,随后给定 \(q\) 次操作,每次操作会修改原字符串上的一个字符,并且你需要回答每次修改后,将此时字符串变为“合法字符串”的最少变换次数,操作不独立。
\(1\le n,q,\le 10^5\)。
思路分析
可以依据三个不同的子任务一步步推出正解。
\(\text{Subtesk 1:}q\le 10\)
注意到修改操作很少。我们可以在每次操作后暴力遍历一遍字符串,求出最少变换次数。
设 \(dp_{i,j}\) 表示将 \(S_{1\sim i}\) 变为“合法字符串”,且 \(S_i\) 为字符 \(j\) 的最少变化次数,显然可以推出转移方程
\]
利用前缀和优化,做一次 dp 的时间复杂度为 \(O(nV)\),其中 \(V=26\),那么整体的时间复杂度为 \(O(qnV)\)。
\(\text{Subtesk 2:}\)保证字符串中仅包含 \(\texttt{a,b,c,d,e}\) 五种字符
dp,带修,不难让人联想到动态 dp。但一次矩阵乘法的时间复杂度为 \(O(V^3)\),\(V=26\) 时无法接受,但 \(V=5\) 时就可以很好的处理了,定义矩阵 \(F_i=\begin{matrix}\left[dp_{i,'a'},dp_{i,'b'},\dots,dp_{i,'e'}\right]\end{matrix}\),再定义 \(\min+\) 矩阵 \(A_i\) 为满足 \(F_{i-1}\times A_i=F_{i}\) 的矩阵,显然可得:
\]
利用线段树维护矩阵乘积,最后转移即可,时间复杂度 \(O(q\log nV^3)\),其中 \(V=5\)。
\(\text{Subtesk 3:}\)无限制
设 \(F(S)\) 表示将 \(S\) 变为“合法字符串的最小操作次数”,设 \(T(S,c)\) 表示将 \(S\) 变为一个 \(01\) 串,其中 \(T(S,c)_i=[S_i\ge c]\),则有如下结论:
\]
证明:
对于任意一种将 \(S\) 变为合法字符串的方案,其总步数 \(F(s)\) 中的第 \(i\) 步将 \(S_{j}\) 变为了 \(S'_{j}\)(只变换成相邻字符),那么这一步会在 \(F(T(S,S'_{j}))\) 中产生一步贡献,反之可以对应。也就是说,将 \(S\) 变为合法字符串且代价最小的这种方案一定对应一种 \(\sum F(T(S,i))\),而前者因为包含后者,所以一定满足 \(\sum F(T(S,i))\le F(S)\)。
何时出现 \(\sum F(T(S,i))<F(S)\) 的情况?因为将 \(S\) 变为合法字符串且代价最小的合法方案为 \(F(s)\),所以满足上式的 \(\sum F(T(S,i))\) 一定对应一种非法方案。如果可以说明最小的 \(\sum F(T(S,i))\) 一定不是非法方案,那么就可以证明上述结论。
我们假设在 \(\sum F(T(S,i))\) 取最优方案下,字符串 \(T(S,i)\) 变为合法字符串后,第一个 \(1\) 的位置是 \(Z_i\),那么一种非法方案必定满足 \(i<j,Z_i>Z_j\)。
因为 \(i<j\),所以 \(T(S,i)\) 中的 \(1\) 一定比 \(T(S,j)\) 中的 \(1\) 多。我们令 \(Z_i=Z_j\) 则一定可以得到一个更优的方案,这与原来的条件矛盾,故假设不成立,原命题成立。
对于 \(F(T(S,i))\),可以看成对只含两种字符的字符串,同样可以用 \(26\) 个线段树分别维护 \(26\) 个 dp,这样的时间复杂度为 \(O(n\log nVb^2)\),其中 \(V=26,b=2\)。
由于时间卡的比较紧,所以提供几点卡常技巧:
- 用 zkw 线段树代替普通线段树。
减小递归常数
- 不再维护矩阵乘积,而是在每个节点 \([l,r]\) 上维护三个值 \(s_0,s_1,res\)。
分别表示将 \([l,r]\) 全部变为 \(0,1\) 的操作数以及将 \([l,r]\) 变为合法字符串的最少操作数,则
\]
\]
\]
很好理解,我们在一个合法字符串前加入一串 \(0\),其依然是合法字符串,在合法字符串后面加上一段 \(1\) 也是如此。这样矩阵的运算量就从 \(16\) 降到 \(4\) 了。
- 避免不必要修改
假设我们将 \(S_i\) 修改为 \(S’_i\),对于字符 \(j<\min\{S_i,S'_i\}\) 或 \(j>\max\{S_i,S'_i\}\),\(T(S,j)\) 不会有变化,我们可以避免这样不必要的修改,大大降低常数。
给出一份参考代码:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,inf=0x3f3f3f3f;
char s[N];
int n,q,ans;
struct mat{
int x0,x1,val;
mat(){
x0=x1=val=0;
}
void init(int x){
if(x==0){
x0=0,x1=1,val=0;
}else{
x0=1,x1=0,val=0;
}
}
mat operator + (const mat &b){
mat ret;
ret.x0=x0+b.x0;
ret.x1=x1+b.x1;
ret.val=min(val+b.x1,x0+b.val);
return ret;
}
};
struct sgt{
mat val[N<<2];
char ch;
int m;
void build(int n){
for(m=1;m<=n;m<<=1);
for(int i=1;i<=n;i++)val[i+m].init(s[i]>=ch);
for(int i=m;i;i--)val[i]=(val[i<<1]+val[i<<1|1]);
}
void change(int x){
val[x+m].init(s[x]>=ch);
for(int i=(x+m)>>1;i;i>>=1)val[i]=(val[i<<1]+val[i<<1|1]);
}
}tr[26];
int main(){
scanf("%s",s+1);
n=strlen(s+1);
for(int i=0;i<26;i++)tr[i].ch='a'+i;
for(int i=0;i<26;i++)tr[i].build(n),ans+=tr[i].val[1].val;
printf("%d\n",ans);
scanf("%d",&q);
while(q--){
int x;
char ch[3];
scanf("%d %s",&x,ch);
ch[1]=s[x],s[x]=ch[0];
if(ch[1]<ch[0])for(int i=ch[1]-'a';i<=ch[0]-'a';i++)tr[i].change(x);
else if(ch[1]>ch[0])for(int i=ch[0]-'a';i<=ch[1]-'a';i++)tr[i].change(x);
ans=0;
for(int i=0;i<26;i++)ans+=tr[i].val[1].val;
printf("%d\n",ans);
}
return 0;
}
农场道路修建
题目大意
给定一棵 \(n\) 个节点,\(n-1\) 条边连接的连通图,你需要从中选出一些点,满足两个限制:
限制 \(1\):不能有两个相邻的点同时选择。
限制 \(2\):在满足限制 \(1\) 的情况下最大化选择的点的数量。
试问存在多少个无序数对 \((i,j)\),满足在加入边 \((i,j)\) 后,新图选出来的点数和原图选出来的点数相同。
\(n\le 10^6\)。
思路分析
加入边 \((i,j)\) 后,树变为基环树。此时就是经典的基环树上的边最大独立集。
基环树上 dp 的一个经典做法是断环成链,我们删去边 \((i,j)\),分别以 \(i,j\) 为根节点做一遍树上的边最大独立集,设 \(f_i,f_j\) 分别表示 \(i,j\) 为根节点,不选 \(i,j\) 的边最大独立集,那么基环树上的边最大权独立集就是 \(\max\{f_i,f_j\}\)。
这样就得到了一个 \(O(n^2)\) 的法,首先以每个点为根节点做树上边最大独立集,求出所有 \(f_i\),然后枚举每一对点对 \((i,j)\),如果 \(\max\{f_i,f_j\}=res\)(\(res\) 是原树上的边最大独立集),那么 \((i,j)\) 就是合法方案的一种。
如果求出 \(f_i\) 后,其实统计答案可以优化到 \(O(n)\),问题是如何快速求出所有 \(f_i\),不难想到换根 dp。
设 \(f_{i,1/0}\) 表示以 \(i\) 为根,\(i\) 选/不选的最大独立集,设 \(g_{i,1/0}\) 表示在 \(i\) 的子树中,\(i\) 选/不选的最大独立集,假设 \(y\) 节点的父亲是 \(x\)。
如果我们已知 \(f_{x,1/0},g_{y,1/0}\),如何计算 \(f_{y,0}\)?
首先,\(f_{y,0}\) 包含 \(g_{y,0}\)。
如果 \(x\) 不选,那么在 \(y\) 子树之外的贡献为 \(f_{x,0}-\max\{g_{y,0},g_{y,1}\}\),不难想,主要思考 dp 自下而上转移选择那个决策,换根时撤销就可以。
如果 \(x\) 选,那么 \(y\) 子树之外的贡献为 \(f_{x,1}-g_{y,0}\)。
可以得到:
\]
\(f_{y,1}\) 很好推,就不推了。
时间复杂度 \(O(n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N=250005;
vector<int>G[N];
int n,f[N][2],g[N][2],res;
long long ans,sum;
void add(int x,int y){
G[x].push_back(y);
}
void dfs1(int x,int fa){
g[x][1]=1,g[x][0]=0;
for(auto y:G[x]){
if(y==fa)continue;
dfs1(y,x);
g[x][0]+=max(g[y][1],g[y][0]);
g[x][1]+=g[y][0];
}
return;
}
void dfs2(int x,int fa){
for(auto y:G[x]){
if(y==fa)continue;
f[y][0]=g[y][0];
f[y][0]+=max(f[x][1]-g[y][0],f[x][0]-max(g[y][0],g[y][1]));
f[y][1]=g[y][1];
f[y][1]+=f[x][0]-max(g[y][0],g[y][1]);
dfs2(y,x);
}
return;
}
int main(){
scanf("%d",&n);
for(int i=1,u,v;i<n;i++){
scanf("%d %d",&u,&v);
add(u,v),add(v,u);
}
dfs1(1,0);
f[1][0]=g[1][0],f[1][1]=g[1][1];
res=max(f[1][0],f[1][1]);
dfs2(1,0);
for(int i=1;i<=n;i++)if(f[i][0]==res)sum++;
for(int i=1;i<=n;i++){
if(f[i][0]==res)ans+=1ll*(n-1);
else ans+=sum;
}
printf("%lld\n",ans/2);
return 0;
}
宵宫和她的烟花(blossom)
题目名忘了,不过好像是这个意思(
题意简述
给定一棵 \(n\) 个点的树,边带权,每个节点上有一个颜色,一共有三种颜色。
定义合法路径为:路径上至少包含三种不同颜色的点的路径;其权值为路径上每条边的权值之和。
求出树上所有合法路径的权值之和,\(n\le 10^5\)。
思路分析
令一条路径在它两个端点的最近公共祖先处产生贡献,假设 \(x,y\) 的最近公共祖先为 \(z\),则 \(x\to y\) 的路径可以拆成 \(x\to z,z\to y\) 两条路径。
对于 \(z\to x\) 这样的路径,我们用一个三位二进制串表示其路径上含有哪几种颜色,记为 \(S(z,x)\),对于 \(z\) 子树内的两个点 \(x,y\),如果他们满足其分别在 \(z\) 的不同子树内,且 \(S(z,x)\cup S(z,y)=\{1,2,3\}\),那么这条路径就会产生贡献。
考虑使用树上动态规划。
定义 \(f_{u,i,s}\) 表示:满足 \(v\) 在 \(u\) 的前 \(i\) 个儿子的子树中,且 \(S(u,v)=s\) 的路径 \(u\to v\) 权值之和。
定义 \(g_{u,i,s}\) 表示:满足 \(v\) 在 \(u\) 的前 \(i\) 个儿子的子树中,且 \(S(u,v)=s\) 的路径 \(u\to v\) 的总数量。
假设节点 \(x\) 的第 \(i+1\) 个子节点 \(y\) 共有 \(j\) 个儿子。那么经过 \((x,y)\) 的合法路径的贡献为:
\]
其中 \(w(x,y)\) 表示 \(x,y\) 之间边的边权。
对于路径 \(u,v\) 满足其经过 \(x\),且一端 \(u\) 在 \(x\) 的前 \(i\) 个子树中,另一端 \(v\) 在 \(y\) 的子树中,\((u,v)\) 的路径数就是 \(g_{y,j,s_2}\times g_{x,i,s_1}\)。随便组合一下就可以得到上式。
对于合并,直接暴力就可以,对于第二维可以直接滚动数组优化掉。
时间复杂度 \(O(n\times 4^k)\),其中 \(k=3\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#define pii pair<int,int>
#define mp make_pair
#define int long long
using namespace std;
const int N=1e5+10,mod=1e9+7;
int n,ans,f[N][8],g[N][8],a[N];
vector<pii>G[N];
void dfs(int x,int fa){
g[x][1<<a[x]]=1;
for(auto [y,z]:G[x]){
if(y==fa)continue;
dfs(y,x);
for(int i=1;i<=7;i++){
for(int j=1;j<=7;j++){
if((i|j)==7&&g[x][i]&&g[y][j]){
ans=(ans+f[x][i]*g[y][j]%mod+f[y][j]*g[x][i]%mod+z*g[x][i]*g[y][j]%mod)%mod;
}
}
}
for(int j=1,i;j<=7;j++){
if(!g[y][j])continue;
i=((1<<a[x])|j);
f[x][i]=(f[x][i]+f[y][j]+z*g[y][j])%mod;
g[x][i]=(g[x][i]+g[y][j])%mod;
}
}
return;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",a+i);
for(int i=1,u,v,w;i<n;i++){
scanf("%lld %lld %lld",&u,&v,&w);
G[u].push_back(mp(v,w));
G[v].push_back(mp(u,w));
}
dfs(1,0);
printf("%lld\n",ans);
return 0;
}
youyou 不喜欢夏天
题意简述
Alice 和 Bob 在一个 \(2\times n\) 的网格上做游戏,网格上只有 \(1,0\) 两种数字,Alice 先选出一些位置,使一个联通块(四联通),Bob 再在这个网格中选出至多 \(m\) 列,将这 \(m\) 列上下翻转,Alice 希望你给出方案,最大化这个 Bob 操作完后网格上这个联通块中 \(1\) 的个数减去 \(0\) 的个数。
\(n,m\le 2\times 10^6\)。
思路分析
首先一个显然的结论,Bob 可以进行操作的列必须满足这一列有一个 \(1\) 一个 \(0\),否则操作没有意义。
显然,一个网格中如果只含有全 \(0\) 列或全 \(1\) 列,是十分好处理的(类似最大子段和)。所以我们考虑如果处理一个 \(1\),一个 \(0\) 的列(下文称这样的列为 \(01\) 列)。
有两种策略:
\(A\):对于一个 \(1\),一个 \(0\) 的列,我们直接两个都选。
\(B\):对于一个 \(1\),一个 \(0\) 的列,我们在联通的基础上尽可能的多选 \(1\) 而少选 \(0\)。
假设 Alice 有 \(a\) 列 \(01\) 列依据策略 \(A\),有 \(b\) 列选择依据策略 \(B\)。策略 \(A,B\) 各有缺点,因为 Bob 可以操作 \(m\) 次,每次使贡献减少 \(2\),而全部使用策略 A 就不会有这种情况,如果 \(a>2m\),那么选择策略 \(B\) 又一定优于策略 \(A\),这使我们很难选择。
关键结论:最优方案下,Alice 要么全选策略 A,要么全选策略 B。
证明:
首先,假设最优方案中既有策略 A,又有策略 B,则一定满足 \(a>2m\)。
如果 \(b\le 2m\),则 Bob 一定可以进行 \(\min(b,m)\) 次操作,使答案减小 \(-2·\min(b,m)\)。对于这种情况,不如这 \(b\) 列都选。答案一定比原方案更优。
此时一定 \(b>2m\)。而 yy 一定会进行 \(m\) 次操作,使这些贡献减小了 \(2m\),如果这样,不如将剩下的 \(a\) 列都只选该列的 \(1\)。这一定会产生新的贡献,同时因为 Bob 已经用光了 \(m\) 次操作,所以对于这些新产生的贡献没办法进行操作。
综上,最优方案下,Alice 要么全选策略 A,要么全选策略 B。
然后动态规划模拟就可以。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=2e6+10;
int ans,n,m,res,T,C,f[N][3];
char s[N],p[N];
void work(){
scanf("%d %d",&n,&m);
scanf("%s",s+1);
scanf("%s",p+1);
for(int i=1;i<=n;i++){
if(s[i]=='1'&&p[i]=='1')res+=2;
else if(s[i]=='0'&&p[i]=='0')res--,res=max(res,0);
ans=max(ans,res);
}
res=0;
for(int i=1;i<=n;i++){
if(s[i]=='1'&&p[i]=='1'){
f[i][0]=max(max(f[i-1][2],f[i-1][0]),0)+1;
f[i][1]=max(max(f[i-1][2],f[i-1][1]),0)+1;
f[i][2]=max(max(f[i-1][2],f[i-1][1]),max(f[i-1][0],0))+2;
}
if(s[i]=='0'&&p[i]=='1'){
f[i][0]=max(max(f[i-1][2],f[i-1][0]),0)-1;
f[i][1]=max(max(f[i-1][2],f[i-1][1]),0)+1;
f[i][2]=max(max(f[i-1][2],f[i-1][1]),max(f[i-1][0],0));
}
if(s[i]=='1'&&p[i]=='0'){
f[i][0]=max(max(f[i-1][2],f[i-1][0]),0)+1;
f[i][1]=max(max(f[i-1][2],f[i-1][1]),0)-1;
f[i][2]=max(max(f[i-1][2],f[i-1][1]),max(f[i-1][0],0));
}
if(s[i]=='0'&&p[i]=='0'){
f[i][0]=max(max(f[i-1][2],f[i-1][0]),0)-1;
f[i][1]=max(max(f[i-1][2],f[i-1][1]),0)-1;
f[i][2]=max(max(f[i-1][2],f[i-1][1]),max(f[i-1][0],0))-2;
}
res=max(max(res,f[i][0]),max(f[i][1],f[i][2]));
}
ans=max(ans,res-2*m);
printf("%d\n",ans);
return;
}
int main(){
scanf("%d %d",&C,&T);
while(T--){
work();
res=0,ans=0;
for(int i=1;i<=n;i++)f[i][0]=f[i][1]=f[i][2]=0;
}
return 0;
}
youyou 的序列 II
题意简述
Alice 和 Bob 在做游戏,他们给定 \(n,q,c_1,c_2,w_1,w_2\)。以及一个长为 \(n\) 的数组 \(a\)。
其中你需要完成两个操作:
- 给定 \(x,y\),使 \(a_x\gets a_x+y\)。
2 给定 \(l,r\),判断在区间 \([l,r]\) 所形成的序列上进行游戏的结果。
其中,在序列 \(a\) 上进行的游戏如下:
Alice 先手,Ta 可以选择一个长度不超过 \(c_1\),\(a_i\) 和不超过 \(w_1\) 的区间打上标记。
Bob 后手,Ta 可以选择一个长度不超过 \(c_2\),\(a_i\) 和超过 \(w_2\) 的区间清除其内标记。
任何一方都可以跳过一个回合自己的操作。
Alice 胜利的条件是 Ta 可以在有限的回合内将整个序列打上标记。
Bob 胜利的条件是 Ta 可以使 Alice 无法在有限回合内达成胜利条件。
\(c_1,c_2,n,q\le 3\times 10^5,0\le w_1,w_2,a_i\le 10^9\)。
思路分析
首先思考 Bob 胜利的局面可能有哪些。
如果 Alice 不可能把整个序列染标记,那么 Bob 躺着就可以赢,出现这种局面则必须满足整个序列的最大值大于 \(w_1\),此时无论如何 Alice 都无法胜利。
如果 Alice 可以把整个序列染标记,那么 Bob 就必须做出一定行动阻止 Alice 达成胜利。因此此时必须有 Bob 有可操作的区间,即区间和大于 \(w_2\) 长度小于 \(c_2\) 的区间,因为数非负,所以 Bob 操作的区间越长越好,即只要满足区间 \([l,l+c_2-1]\) 满足 \(S_{l+c_2-1}-S_{l-1}>w_2\),其中 \(S\) 是前缀和数组。
如果存在一个 Alice 不能操作,但 Bob 可操作的区间,则 Alice 必输,因为无论如何它都不能使 Bob 可操作的区间都打上标记。
那么如果 Bob 可操作的所有区间,Alice 都可以操作呢?
结论:假设 Bob 有 \(k\) 个可操作的区间 \([l_1,r_1],[l_2,r_2],\dots,[l_k,r_k]\),如果 Alice 无法通过一次操作将 \([l_1,r_k]\) 内全部标记,那么 Alice 必输。
证明:非常好证,如果 Alice 将 \(l_1\) 打上标记,下一步 Bob 就可以清除 \(r_k\) 上的标记;反之,若 Alice 将 \(r_k\) 打上标记,下一步 Bob 就可以清除 \(l_1\) 上的标记。如此循环,Alice 注定无法在有限的回合内取胜。
综上可得:
若区间最大值大于 \(w_1\),则 Bob 必胜。
若 Bob 无可操作的区间,则 Alice 必胜。
假设 Bob 有 \(k\) 个可操作的区间 \([l_1,r_1],[l_2,r_2],\dots,[l_k,r_k]\),若 Alice 无法操作 \([l_1,r_k]\) 这个区间,则 Bob 必胜。
Alice 必胜。
只需按照顺序模拟,就可以得到 50 pts。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=3e5+10;
typedef long long ll;
ll n,q,c1,c2,w1,w2,a[N],op,l,r,s[N],len,lef,righ,flag;
int main(){
scanf("%lld %lld %lld %lld %lld %lld",&n,&q,&c1,&c2,&w1,&w2);
for(int i=1;i<=n;i++){
scanf("%lld",a+i);
}
while(q--){
scanf("%lld %lld %lld",&op,&l,&r);
if(op==1)a[l]+=r;
else{
for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
len=min(r-l+1,c2),lef=righ=flag=0;
for(int i=l;i<=r;i++)if(a[i]>w1)flag=1;
if(flag){
printf("tetris\n");
continue;
}
for(int i=l+len-1;i<=r;i++){
if(s[i]-s[i-len]>w2){
if(!lef)lef=i-len;
righ=i;
}
}
if(!lef&&!righ)printf("cont\n");
else if(righ-lef<=c1&&s[righ]-s[lef]<=w1)printf("cont\n");
else printf("tetris\n");
}
}
return 0;
}
思考如何快速维护。
首先可以用线段树维护区间最大值。
然后用另一颗线段树维护数组 \(v\),其中 \(v_i=S_i-S_{i-c_2}\)。单点查询就可以转化为区间加,同时维护区间最大值。然后线段树二分即可找到 \(l_1,r_k\)。时间复杂度 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#define ls (p<<1)
#define rs (p<<1|1)
using namespace std;
const int N=3e5+10;
typedef long long ll;
int n,q,c1,c2,op,l,r,lef,righ;
ll w1,w2,s[N],val,tmp;
struct sgt1{
ll mx[N<<2],sm[N<<2];
void push_up(int p){
mx[p]=max(mx[ls],mx[rs]);
sm[p]=sm[ls]+sm[rs];
}
void add(int p,int l,int r,int x,ll v){
if(l==r){
mx[p]+=v,sm[p]+=v;
}else{
int mid=(l+r)>>1;
if(x<=mid)add(ls,l,mid,x,v);
if(x>mid)add(rs,mid+1,r,x,v);
push_up(p);
}
return;
}
ll ask(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return mx[p];
int mid=(l+r)>>1;ll cnt=0;
if(L<=mid)cnt=max(cnt,ask(ls,l,mid,L,R));
if(R>mid)cnt=max(cnt,ask(rs,mid+1,r,L,R));
return cnt;
}
ll sum(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return sm[p];
int mid=(l+r)>>1;ll cnt=0;
if(L<=mid)cnt+=sum(ls,l,mid,L,R);
if(R>mid)cnt+=sum(rs,mid+1,r,L,R);
return cnt;
}
}a;
struct sgt2{
ll mx[N<<2],tag[N<<2];
void push_up(int p){
mx[p]=max(mx[ls],mx[rs]);
}
void push_down(int p){
if(!tag[p])return;
tag[ls]+=tag[p],tag[rs]+=tag[p];
mx[ls]+=tag[p],mx[rs]+=tag[p];
tag[p]=0;return;
}
void add(int p,int l,int r,int L,int R,ll v){
if(L<=l&&r<=R){
mx[p]+=v,tag[p]+=v;
}else{
int mid=(l+r)>>1;
push_down(p);
if(L<=mid)add(ls,l,mid,L,R,v);
if(R>mid)add(rs,mid+1,r,L,R,v);
push_up(p);
}
}
int find_left(int p,int l,int r,int L,int R,ll v){
if(L<=l&&r<=R){
if(mx[p]<=v)return -1;
if(l==r)return l;
push_down(p);int mid=(l+r)>>1;
if(mx[ls]>v)return find_left(ls,l,mid,L,R,v);
else return find_left(rs,mid+1,r,L,R,v);
}
int mid=(l+r)>>1,res=-1;push_down(p);
if(L<=mid)res=find_left(ls,l,mid,L,R,v);
if(res!=-1)return res;
if(R>mid)res=find_left(rs,mid+1,r,L,R,v);
return res;
}
int find_right(int p,int l,int r,int L,int R,ll v){
if(L<=l&&r<=R){
if(mx[p]<=v)return -1;
if(l==r)return l;
push_down(p);int mid=(l+r)>>1;
if(mx[rs]>v)return find_right(rs,mid+1,r,L,R,v);
else return find_right(ls,l,mid,L,R,v);
}
int mid=(l+r)>>1,res=-1;push_down(p);
if(R>mid)res=find_right(rs,mid+1,r,L,R,v);
if(res!=-1)return res;
if(L<=mid)res=find_right(ls,l,mid,L,R,v);
return res;
}
}b;
int main(){
scanf("%d %d %d %d %lld %lld",&n,&q,&c1,&c2,&w1,&w2);
for(int i=1;i<=n;i++){
scanf("%lld",s+i);
a.add(1,1,n,i,s[i]);
s[i]+=s[i-1];
}
for(int i=c2;i<=n;i++)b.add(1,1,n,i,i,s[i]-s[i-c2]);
c1=min(c1,n),c2=min(c2,n);
while(q--){
scanf("%d %d",&op,&l);
if(op==1){
scanf("%lld",&val);
a.add(1,1,n,l,val);
b.add(1,1,n,l,min(n,l+c2-1),val);
}else{
scanf("%d",&r);
if(a.ask(1,1,n,l,r)>w1){
printf("tetris\n");
continue;
}
if(r-l+1<=c2){
tmp=a.sum(1,1,n,l,r);
if(tmp>w2){
if(r-l+1<=c1&&tmp<=w1)printf("cont\n");
else printf("tetris\n");
}else printf("cont\n");
}else{
lef=b.find_left(1,1,n,l+c2-1,r,w2);
righ=b.find_right(1,1,n,l+c2-1,r,w2);
if(lef==-1&&righ==-1)printf("cont\n");
else{
lef-=c2;
if(righ-lef<=c1&&a.sum(1,1,n,lef+1,righ)<=w1)printf("cont\n");
else printf("tetris\n");
}
}
}
}
return 0;
}
The End
终于写完了。
本篇收录了 6 道题目,全部来源于模拟赛,我个人认为有一定的思维难度。话说怎么没有数学题,以 DS,dp 为主。
明天 CSP2024 复赛,祝各位 OIer 们 RP++!!!
愿我们都有美好明天。
杂题 I的更多相关文章
- 正睿OI DAY3 杂题选讲
正睿OI DAY3 杂题选讲 CodeChef MSTONES n个点,可以构造7条直线使得每个点都在直线上,找到一条直线使得上面的点最多 随机化算法,check到答案的概率为\(1/49\) \(n ...
- dp杂题(根据个人进度选更)
----19.7.30 今天又开了一个新专题,dp杂题,我依旧按照之前一样,这一个专题更在一起,根据个人进度选更题目; dp就是动态规划,本人认为,动态规划的核心就是dp状态的设立以及dp转移方程的推 ...
- wangkoala杂题总集(根据个人进度选更)
CQOI2014 数三角形 首先一看题,先容斥一波,求出网格内选三个点所有的情况,也就是C(n*m,3);然后抛出行里三点共线的方案数:C(n,3)*m; 同理就有列中三点共线的方案数:n*C(m,3 ...
- 2019暑期金华集训 Day6 杂题选讲
自闭集训 Day6 杂题选讲 CF round 469 E 发现一个数不可能取两次,因为1,1不如1,2. 发现不可能选一个数的正负,因为1,-1不如1,-2. hihoCoder挑战赛29 D 设\ ...
- Atcoder&CodeForces杂题11.7
Preface 又自己开了场CF/Atcoder杂题,比昨天的稍难,题目也更有趣了 昨晚炉石检验血统果然是非洲人... 希望这是给NOIP2018续点rp吧 A.CF1068C-Colored Roo ...
- Codeforces 杂题集 2.0
记录一些没有写在其他随笔中的 Codeforces 杂题, 以 Problemset 题号排序 1326D2 - Prefix-Suffix Palindrome (Hard version) ...
- 【Java面试】-- 杂题
杂题 2019-11-03 21:09:37 by冲冲 1.类加载器的双亲委派机制 类加载器:把类通过类加载器加载到JVM中,然后转换成class对象(通过类的全路径来找到这个类). 双亲委派机制 ...
- 贪心/构造/DP 杂题选做Ⅱ
由于换了台电脑,而我的贪心 & 构造能力依然很拉跨,所以决定再开一个坑( 前传: 贪心/构造/DP 杂题选做 u1s1 我预感还有Ⅲ(欸,这不是我在多项式Ⅱ中说过的原话吗) 24. P5912 ...
- 贪心/构造/DP 杂题选做Ⅲ
颓!颓!颓!(bushi 前传: 贪心/构造/DP 杂题选做 贪心/构造/DP 杂题选做Ⅱ 51. CF758E Broken Tree 讲个笑话,这道题是 11.3 模拟赛的 T2,模拟赛里那道题的 ...
- [POJ&HDU]杂题记录
POJ2152 树形dp,每次先dfs一遍求出距离再枚举所有点转移即可. #include<iostream> #include<cstdio> #include<cma ...
随机推荐
- issue: java.lang.NoClassDefFoundError: javax/el/ELManager
问题描述: Context initialization failed org.springframework.beans.factory.BeanCreationException: Error c ...
- AI提示词:一个通用C++ ECS系统实现(事件条件动作系统)
AI提示词 using eca_cond = bool(*)(...); using eca_action = void(*)(...); class eca_info { public: eca_c ...
- RandomWalk随机游走
RandomWalk随机游走: 在自然界,物理学,生物学,化学,经济学等众多领域,随机游走都有实际的用途,例如,其可以描述一个漂浮在水滴上的花粒因受到水分子的作用力而在水滴表面随机移动.诸如此类的不规 ...
- Sentinel——服务降级
目录 简介 Sentinel方法级降级 Sentinel类级降级 OpenFeign类级降级 简介 服务降级是一种增强用户体验的方式.当用户的请求由于各种原因被拒后,系统返回-一个事先设定好的.用户可 ...
- 代码随想录第二天 | Leecode 209. 长度最小的子数组、59. 螺旋矩阵II
Leecode 209 长度最小的子数组 题目链接:https://leetcode.cn/problems/minimum-size-subarray-sum/ 题目描述 给定一个含有 n 个正整数 ...
- HarmonyOS NEXT开发实战教程:选择相册和拍照
今天的内容是介绍在鸿蒙开发中从相册选择照片,和调用相机拍照,并使用这两个功能实现朋友圈编辑页面. 这部分内容没什么好废话的,都是固定用法,直接上代码.首先添加权限: ohos.permission.C ...
- 在鸿蒙Next中开发一个月历组件
最近一直在出差,工作繁忙,很久没有时间更新文章了,连华为开发者大会也错过了.今天周末,忙里偷闲给大家分享一个鸿蒙月历组件. 这样的组件大家在工作中应该经常会遇到,而鸿蒙又没有提供一个这样的系统组件,今 ...
- AD 侦查-LLMNR 毒化
本文通过 Google 翻译 AD Recon – LLMNR Poisoning with Responder 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释补充. 导航 ...
- Mac M1 安装python3.6.x
在mac M1上通过pyvenv 直接安装python3.6.x 会失败. 后来发现其实python官方直接提供了m1的pkg包,就不需要再重新编译安装了. 进入python官方为macos提供的各版 ...
- 把iview的table做成更适合展现大量数据的样式(字体变小、去除多余的padding等)
<style> .ivu-table { font-size: 12px !important; } .ivu-table-header thead tr th { padding: 0p ...