SBC的水题大赛题解
比赛链接
T1 小镇 题解
对于 30\% 的数据,1 \leq n,k \leq 100 。
没啥注意的,算法同
对于 80\% 的数据,1 \leq n,k \leq 5000 。
我们可以暴力枚举任意两个被涂色的格子,看他们是否相邻。
对于两个相邻的格子,可以注意到他们行相同,列差为一 或者 列相同,行差为一。所以我们可以通过
时间复杂度
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int n,k,x[MAXN],y[MAXN],ans=0;
int main()
{
n=read();k=read();
for(int i=1;i<=k;i++)
{
x[i]=read();
y[i]=read(); //读入
}
for(int i=1;i<=k;i++)
for(int j=i+1;j<=k;j++) //暴力枚举任意两个格子
{
if(abs(x[i]-x[j])+abs(y[i]-y[j])==1) //判断相邻
ans++;
}
cout<<ans<<endl;
return 0;
}
对于 100\% 的数据,1 \leq n,k \leq 10^5 。
我们把两个相邻格子的公共边成为夹边。
观察样例,可以发现对于每条夹边,都平行于x轴或者平行于y轴。所以我们可以自然想到对于平行于x轴和y轴的夹边分开计算。
接下去步骤就比较明了了,对于平行于x轴的边,我们把格子的x轴作为第一关键字排序,再根据y轴排序。
这样,就保证了相邻的两个格子如果x轴相同,那他们的y轴单调递增。
这时候我们比较y轴是否差一,便能统计出所有平行于x轴的夹边了。平行于y轴的边同理,具体见代码。
时间复杂度
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
struct node
{
int x,y;
}a[MAXN];
bool cmp1(node a,node b)
{
if(a.x!=b.x) return a.x<b.x;
else return a.y<b.y;
}
bool cmp2(node a,node b)
{
if(a.y!=b.y) return a.y<b.y;
else return a.x<b.x;
} //排序函数
int n,k,ans;
int main()
{
n=read();k=read();
for(int i=1;i<=k;i++)
{
int x,y;
x=read();y=read();
a[i].x=x;a[i].y=y;
}
sort(a+1,a+k+1,cmp1); //统计平行于x轴的边
for(int i=1;i<k;i++)
{
if(a[i].x==a[i+1].x && a[i].y==a[i+1].y-1) //若x轴相同并且y轴差为1
ans++;
}
sort(a+1,a+k+1,cmp2); //统计平行于y轴的边
for(int i=1;i<k;i++)
{
if(a[i].y==a[i+1].y && a[i].x==a[i+1].x-1) //同上
ans++;
}
cout<<ans<<endl;
return 0; //qwq
}
T2 时光的流逝 题解
这道题其实本质就是个图上的博弈论
对于 10\% 的数据,保证图是一条链。
由于是一条链,所以情况是唯一的,直接链表模拟即可。具体见代码。
代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,q,nxt[500005],a,b;
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=m;i++)
{
cin>>a>>b;
nxt[a]=b; //由于是链表
}
for(int i=1;i<=q;i++)
{
cin>>a>>b;
int k=-1,x=a,f=0; //x表示当前位置,k表示当前走棋的人
while(nxt[x])
{
k=-k; //每次转换走棋的人
x=nxt[x]; //走到下一个位置
if(x==b) //到终点
{
cout<<k<<endl;
f=1;
break;
}
}
if(!f)
cout<<k<<endl; //如果走到终点,则无法动的人输
}
return 0;
}
不难发现这个题是个图上的博弈论的问题。我们自然可以考虑到在图上标记必胜点以及必败点。
首先终点为必败点,所有出度为0的点均为必败点,然后所有能一步到达必败点的点为必胜点,下一步只能到必胜点的点为必败点。于是我们便可以根据这个规则给所有点标记,最后如果起点没有标到,那么就平局。
给张图理解下:
这里有一张图,终点为
首先,终点10为必败点。9,5能到达10,所以9,5为必胜点。
7,4由于是死路(及走到这个点无路可走)为必败点,标记为红色,于是3,6能到达4,7,故为必胜点。
8能到达的所有点(6)均为必胜点,所以8为必败点。
1能到达必败点8,所以1为必胜点。
2能到达的所有点(1)均为必胜点,所以2为必败点。
这样我们就能得到所有起点的情况了。
对于 50\% 的数据,1\leq n\leq 10^3 ,1\leq m\leq 2\times10^3 ,1\leq q\leq 10 。
这一档主要就是对于上面那种方法比较暴力的解决方法。每次暴力枚举所有点,看看是否能更新,知道不能更新为止。复杂度
对于 70\% 的数据,1\leq n\leq 10^5 ,1\leq m\leq 2\times10^5 ,1\leq q\leq 10 。
这一档就是给一些常数比较大的做法,或者是一些玄学带 log 做法的做法。比如用优先队列判断度数最小的点以及,起点终点搜两遍的做法。
对于 100\% 的数据,1\leq n\leq 10^5 ,1\leq m\leq 5\times10^5 ,1\leq q\leq 500 。
我们可以发现只要一个点的所有出点的状态(即确定是否为必胜点或必败点)时,那个点的状态我们便能确定。因为若它有一条边指向必败点,则它为必胜点。若它所有边都指向必胜点,那么他就是必败点。所以我们就能想到然后建反向边(因为我们需要从已经确定的点去修改未知的点,即需要知道哪些点会通到它),用一个数组记录它的出度,一个数组记录当前的标记(必胜点或必败点)。
我们用一个队列保存当前可以确定状态的点(即出度为0的点,由于我们只需要讨论反向边的图,所以这里出度即为原图的入度),如果找到一个必败点,那么立即修改所有能通到它的点,把这些点标记为必胜点,并且将能通到这些必胜点的点出度减一。同时我们在减去出度的时候看出度是否为0,如果为0,那么这个点的状态即可确定,放进队列。相当于将已经确定状态的点从图中删去。时间复杂度
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5+5;
const int MAXM = 5e5+5;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int n,m;
int p[MAXN],vic[MAXN],out[MAXN];
int cnt,head[MAXM],f[MAXN],d[MAXN];
struct edge
{
int v,nxt;
}e[MAXM*2];
inline void addedge(int u,int v)
{
cnt++;
e[cnt].v=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
queue<int> q;
void del(int u)
{
f[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
d[v]--;
if(d[v]==0)
q.push(v);
}
} //当前点已经确定状态点删去
int main()
{
int Q;
n=read();m=read();Q=read();
for(int i=1;i<=m;i++)
{
int a,b;
a=read();b=read();
addedge(b,a); //建反向边
p[a]++; //入度
out[b]++; //出度
}
while(Q--)
{
int x,y;
while(!q.empty()) q.pop();
x=read();y=read();
memset(f,0,sizeof(f));
memset(d,0,sizeof(d));
memset(vic,0,sizeof(vic)); //初始化
for(int i=1;i<=n;i++)
{
d[i]=p[i];
if(p[i]==0)
q.push(i); //若当前点出度为0,放进队列
}
q.push(y); //将终点放进队列
vic[y]=1; //终点为必败点
while(!q.empty())
{
int u=q.front();
q.pop();
if(f[u]==1)
continue; //已经被访问过
if(vic[x]!=0)
break; //小优化,若起点状态已经确定,那就不需要继续搜索了
del(u); //u已经能确定状态了,将它删去
if(vic[u]==1)//如果u为必败点,那么所有能通往它的点为必胜点
{
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(vic[v]==0)
{
vic[v]=-1;
del(v); //v为必胜点,状态确定,将它删去
}
}
}
else if(out[u]==0)
{
vic[u]=1; //若u在原图出度为0,则为必败点
}
else //u状态未确定
{
vic[u]=1; //u能走到的所有点为必胜点,u为必败点
for(int i=head[u];i;i=e[i].nxt) //将所有能通到必败点标为必胜点
{
int v=e[i].v;
if(vic[v]==0)
{
vic[v]=-1;
del(v); //删去
}
}
}
}
cout<<-vic[x]<<endl; //输出起点状态
}
return 0;
}
T3 人 题解
Subtask 1 (10\%) ,1 \le T \le 10, 1 \le a,b \le m \le 10 。
对于这一部分,
代码:
#include <bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int Q,m,a,b,ans,l[105],r[105];
//l数组表示奇数已经选取的数字,r数组表示偶数已经选取的数字
bool vis[105]; //保存当前数字是否可用
void dfs(int m,int x,int y)
{
if(x==a+1 && y==b+1) //如果已经选完了
{
ans++;
return;
}
if(x==a+1) //如果奇数选完了,选偶数
{
for(int i=r[y-1]/2+1;i<=m;i++) //从上一次选的地方开始选
{
if(vis[2*i]==0) //当前数可用
{
vis[2*i]=1;
r[y]=i*2; //记录
dfs(m,x,y+1); //dfs下一层
vis[2*i]=0; //回溯
}
}
}
else //选奇数
{
for(int i=l[x-1]/2;i<m;i++)
{
if(vis[2*i+1]==0) //同上
{
vis[2*i]=vis[2*i+1]=vis[2*i+2]=1; //由于不能选相邻的数,则2*i+1周围的数也不能选
l[x]=i*2+1;
dfs(m,x+1,y);
vis[2*i+1]=0; //回溯
if(i==0 || vis[2*i-1]==0) //如果2*i左右两边都没有数,她才可以继续使用
vis[2*i]=0;
if(vis[2*i+3]==0) //此时2*i+1不选,如果2*i+3也不选,那么2*i+2可以使用
vis[2*i+2]=0;
}
}
}
}
int main()
{
Q=read();
while(Q--)
{
m=read();a=read();b=read();
memset(vis,0,sizeof(vis)); //初始化
ans=0;
dfs(m,1,1);
cout<<ans<<endl;
}
return 0;
}
Subtask 2 (40\%) ,1 \le T \le 10^6, 1 \le a,b \le m \le 100 。
我们可以发现
我们用
对于
- 在1到2(m-1)中选a个奇数,b个偶数。此时答案为dp[m-1][a][b]
- 在1到2(m-1)中选a个奇数,b-1个偶数,在 2m-1,2m 中选 2m 这个偶数。此时答案为 dp[m-1][a][b-1]
- 在1到2(m-1)中选a-1个奇数,b个偶数,在 2m-1,2m 中选 2m-1 这个奇数。此时情况数为 dp[m-1][a-1][b]。但是这里还有另外一个条件,因为我们选了 2m-1 , 也就是说 2m-2 这个数我们不能选。所以我们需要把选了 2m-2 的情况排除掉。也就是说排除前 2m-2 个数中选了 2m-2 的情况。此时前 2m-4 个数可以随便选,所以情况为 dp[m-2][a-1][b-1]。所以这种情况最终的答案为 dp[m-1][a-1][b]-dp[m-2][a-1][b-1]
所以我们可以得到递推式子
初始化时,
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MAXM = 105;
const ll MOD = 998244353;
inline ll read()
{
ll x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
ll dp[MAXM][MAXM][MAXM];
int main()
{
ll T;
T=read();
dp[1][0][0]=dp[1][1][0]=dp[1][0][1]=1;
dp[2][1][1]=1; //初始化
for(ll i=2;i<=100;i++)
{
dp[i][0][0]=1;
for(ll j=1;j<=i;j++)
{
dp[i][0][j]=(dp[i-1][0][j]+dp[i-1][0][j-1])%MOD;
dp[i][j][0]=(dp[i-1][j][0]+dp[i-1][j-1][0])%MOD;
//dp[i][0][j]=dp[i][j][o]=C(i,j) 这里我用了递推实现,C(i,j)=C(i-1,j)+C(i-1,j-1)
}
}
for(ll i=3;i<=100;i++)
{
for(ll a=1;a<=100;a++)
{
for(ll b=1;a+b<=i;b++)
{
dp[i][a][b]=(dp[i-1][a][b]+dp[i-1][a-1][b]+dp[i-1][a][b-1]-dp[i-2][a-1][b-1]+MOD)%MOD; //dp,这里如果不加+MOD再%MOD可能会出现负数的情况
}
}
}
while(T--)
{
ll m,a,b;
m=read();
a=read();
b=read();
cout<<dp[m][a][b]<<endl; //每次询问O(1)
}
return 0;
}
Subtask 3 (50\%) ,1 \le T \le 10^6, 1 \le a,b \le m \le 10^6 。
我们发现
观察数据,我们可以发现
下面我们来证明一下。需要用到组合数性质
上一个子问题,我们得到递推公式
显然,当
对
我们将公式带入右边。
所以我们就证明了这个结论。对于每个问题,我们只需要求出
求组合数的办法我用的是线性预处理逆元,不了解的可以看这里
组合数
若
别忘了
代码:
#include <bits/stdc++.h>
#define ll long long
const int MAXN = 1e6+5;
using namespace std;
inline ll read()
{
ll x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
const ll MOD = 998244353;
ll a[MAXN],ny[MAXN],Q,n,t,s;
ll ans,d,e,f,g,h;
int main()
{
Q=read();
a[0]=1;
ny[1]=1;
for(int i=1;i<MAXN;i++)
a[i]=((ll)i*a[i-1])%MOD;
for(int i=2;i<MAXN;i++)
ny[i]=((ll)(MOD-MOD/i)*ny[MOD%i]%MOD);
for(int i=2;i<MAXN;i++)
ny[i]=ny[i]*ny[i-1]%MOD; //预处理逆元
while(Q--)
{
n=read();t=read();s=read();
if(n==t+s)
{
printf("1\n");
continue;
} //如果n=t+s那么只有一种情况
ans=a[n-s]*a[n-t]%MOD;
ans=ans*ny[t]%MOD;ans=ans*ny[s]%MOD;
ans=ans*ny[n-t-s]%MOD;ans=ans*ny[n-t-s]%MOD; //求组合数
printf("%lld\n",ans);
}
return 0;
}
T4 归家之路 题解
令序列长
首先对于前
暴力枚举子集的方法是(这里是 c++ 代码)
int f=x^y;
for(int t=x^y;t>=0;t=(t-1)&f)
当然还有什么都不发生的情况,也就是不存在满足要求的 a and b=a 进行特判。
继续开始思考后面的部分分。之后都假设所有的修改和询问都是有效的,也就是 a and b=a一直成立。
这个修改、询问的形式类似一个区间,那么需要知道一个前置知识 高维前缀和。如果不知道的话可以学一下,比较简单。
那么类比不带修直接求区间的值,我们考虑用几个高维前缀和相加减去表示一个询问的答案。
这里可以从简单的情况开始考虑。如果 c and b=b。那么这个答案比 c and b=b 的位数之和。后面的那个恰好可以用
思路逐渐明朗起来,如果 1 的一位的值,那么我们可以得到
这个式子可以位运算进行优化,x=a&-a,a-x 可以用 a^x 表示。
这个看似还没啥用——如果
如果
进行特判并且选择可以做到
这里有一个很小的优化,没什么用:
可以快速预处理出 1,这样查询二进制 1 的个数就从 FFT 中的那个 rev 数组类似:
for(int i=0;i<(1<<n);i++)count[i]=count[i>>1]+(i&1),rev[i]=(1<<n)-i-1;
其实这么进行修改也是 curr 记录。如果 curr[i] 记录了值,那么就代表所有 i 的子集都要加上这个值。
这里用代码更清楚一点:
void modify(int x,int y,uint z)
{
if(count[x^y]<=n/2)
{
int f=x^y;
for(int t=x^y;;t=(t-1)&f){
a[t|x]+=z;
if(t==0)
break;
//这一部分是对修改值不多的情况进行暴力
}
return;
}
if(x==0)
{
curr[y]+=z;//如果x=0那么就可以直接打上懒标记
return;
}
int lbt=x&-x;
modify(x^lbt,y,z);
modify(x^lbt,y^lbt,-z);//与 query 部分类似的递归
}
等到所有修改结束后可以下放懒标记,暴力下放的复杂度是
然而这里并不是没有优化的空间了。下放懒标记的过程完全可以看成又一次高维前缀和。
本来的高维前缀和上的值是它的所有子集的和;而这时候需要加的值,是所有以它为自己的懒标记的和。
举个栗子,如果一个数的二进制表示是 001,那么高维前缀和中就是 000,001 的和,在这种情况下要加上 001,011,101,111 这四个懒标记的和。所以这样只用反着来一遍高维前缀和就行了。
为了加速,我预处理了 rev 数组代表一个数二进制取反后的结果,因为这里不能直接用 ~。(这个 rev 不是 FFT 中的 rev)
话不多说上代码:
void update()
{
for(int i=0;i<n;i++)
for(int j=0;j<(1<<n);j++)
{
if(j&(1<<i))
curr[rev[j]]+=curr[rev[j^(1<<i)]];
}
//这里是反着的高维前缀和
for(int i=0;i<(1<<n);i++)
a[i]+=curr[i];
for(int i=0;i<(1<<n);i++)
pres[i]=a[i];
for(int i=0;i<n;i++)
for(int j=0;j<(1<<n);j++)
if(j&(1<<i))
pres[j]+=pres[j^(1<<i)];
}
其中 pres 是高维前缀和数组。这一次 update 的复杂度是高维前缀和的复杂度
好了,各位大佬们看到这里,正解已经呼之欲出了:分块!
我们假设块长为
修改还是正常修改,查询还是正常查询。这里复杂度是
查询的时候,唯一的问题就是计算块内贡献。这个可以位运算 update 的复杂度是
块内的贡献还是有细节的。大概是这两种情况:
(1)一个修改的数和询问的数没有相同的。
(2)有一部分相同。
这个位运算有很多种实现方法,直接上代码:
for(int i=1;i<=cnt;i++)
{
num=(s[i].a|a)&(s[i].b&b);
if(num==int(s[i].a|a))
{
num=(s[i].a|a)^(s[i].b&b);
ans+=(1<<count[num])*s[i].k;
}
}
cout<<ans<<endl;
std 的长度 2235byte,作为数据结构还是比较良心的。
T5 一直在你身旁 题解
首先有个
枚举区间长度,或者倒序枚举
考虑优化,爆推式子好像有点难,那么观察式子,里面有一个
想到这里,接下来的部分就顺理成章了,分类讨论中转点
(1)vector 优化到
(2)
代码不长,即使手写单调队列优化常数都只有 30 行。
值得一提的是,如果令
并且,