2024.7 总结
图论
【ARC173D】 Bracket Walk
题目描述
给定一张 \(N\) 个点 \(M\) 条边的有向图,每条边上都有一个字符 (
或 )
,图上无自环无重边。
保证整张图是强连通的,也就是任意两点 \(s,t\) 间均至少存在一条 \(s\) 到 \(t\) 的路径。
问是否存在一条路径使得:
-
路径的起点与终点相同。
-
每条边至少在路径中被经过一次。
-
路径经过的边上的字符依次连接形成一个合法括号序列。
\(2 \leq N \leq 4000\),\(N \leq M \leq 8000\)。
解题思路
首先我们能发现一个结论:对于一个环,将环上的 \((\) 看成 \(+1\) ,\()\) 看成 \(-1\) ,只要这个环的权值和为 \(0\) ,那么一定能通过旋转的方式来使它成为一个合法环。
这个结论很好理解,直接旋至最低点处即可。
题目让我们找一个经过所有边的环,由于可以重复经过一条边,我们可以联想到正\(/\)负环。
我们可以想到,满足题目要求的图,要不没有正环与负环,要么正环与负环都有。
这两个都很好像,那么只有一种环的肯定没法找到一个经过全部边的环使得权值和为 \(0\) 。
只需要用 \(BF\) 找最短路判正\(/\)负环即可。
时间复杂度 \(O(nm)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
int x,y,v;
}a[8005];
int n,m,f1[4005],f2[4005];
int main()
{
char z;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
cin>>a[i].x>>a[i].y>>z;
if(z=='(')a[i].v=1;
else a[i].v=-1;
}
memset(f1,0x7F,sizeof(f1)),memset(f2,0x80,sizeof(f2));
f1[1]=f2[1]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)f1[a[j].y]=min(f1[a[j].y],f1[a[j].x]+a[j].v),f2[a[j].y]=max(f2[a[j].y],f2[a[j].x]+a[j].v);
}
bool q1=false,q2=false;
for(int i=1;i<=m;i++)
{
if(f1[a[i].y]>f1[a[i].x]+a[i].v)q1=true;
if(f2[a[i].y]<f2[a[i].x]+a[i].v)q2=true;
}
if(q1==q2)printf("Yes");
else printf("No");
return 0;
}
数据结构
【CF380C】 Sereja and Brackets
题目描述
- 本题中「合法括号串」的定义如下:
- 空串是「合法括号串」。
- 若 \(s\) 是「合法括号串」,则 \((s)\) 是「合法括号串」。
- 若 \(s,t\) 是「合法括号串」,则 \(st\) 是「合法括号串」。
- 有一个括号串 \(s\)。\(m\) 次操作。操作有一种:
l r
:求字符串 \(t=s_ls_{l+1}\cdots s_r\) 的所有 子序列 中,长度最长的「合法括号串」,输出长度即可。
- \(1\le |s|\le 10^6\),\(1\le m\le 10^5\)。
解题思路
首先,将 \((,)\) 转化成 \(+1,-1\) ,就是找一个子序列,使得所有的前缀和 \(\ge 0\) ,最后一个前缀和为 \(0\) 。
我们考虑使用线段树,维护没能匹配的 \((\) 数量与没能匹配的 \()\) 数量。
每次两个区间合并时,就是将两区间没匹配的 \(()\) 个配对了进行计算即可。
由于我们默认没匹配的 \((\) 尽量靠前,没匹配的 \()\) 尽量靠后,不会出现匹配不了的问题。
时间复杂度 \(O(nlogn)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
int v1,v2;
}f[4000005];
string a;
int n,m;
void build(int x,int l,int r)
{
if(l==r)
{
if(a[l]=='(')f[x].v1=1,f[x].v2=0;
else f[x].v1=0,f[x].v2=1;
return;
}
int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
build(lc,l,mid),build(rc,mid+1,r);
f[x].v1=f[lc].v1+f[rc].v1-min(f[lc].v1,f[rc].v2);
f[x].v2=f[lc].v2+f[rc].v2-min(f[lc].v1,f[rc].v2);
return;
}
datay query(int x,int l,int r,int ql,int qr)
{
if(ql<=l&&r<=qr)return f[x];
int mid=(l+r)>>1,lc=(x<<1),rc=(x<<1)|1;
if(qr<=mid)return query(lc,l,mid,ql,qr);
if(ql>mid)return query(rc,mid+1,r,ql,qr);
datay z,q=query(lc,l,mid,ql,qr),w=query(rc,mid+1,r,ql,qr);
z.v1=q.v1+w.v1-min(q.v1,w.v2);
z.v2=q.v2+w.v2-min(q.v1,w.v2);
return z;
}
int main()
{
int x,y;datay z;
cin>>a,n=a.size();
a=' '+a,build(1,1,n);
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y),z=query(1,1,n,x,y);
printf("%d\n",y-x+1-z.v1-z.v2);
}
return 0;
}
【梦熊】 对称之美
题目描述
给出 \(n,k\) \((1 \le n,k \le 10^{14})\) ,求 \(\sum_{i=0}^{k-1} C_{2i}^i n^i (mod\) \(k)\) 。
解题思路
首先,枚举可得,\(k=2\) 时答案为 \(1\) 。
带回原式并化简,即为:
结合二项式定理,由于 \(k\) 较大,需用龟速乘,结合快速幂 \(O(log^2n)\) 解决。
Code
#include<bits/stdc++.h>
using namespace std;
unsigned long long mod;
unsigned long long mul(unsigned long long x,unsigned long long y)
{
unsigned long long h=0;
while(y>0)
{
if(y&1)h=(h+x)%mod;
x=(x+x)%mod,y/=2;
}
return h;
}
unsigned long long poww(unsigned long long x,unsigned long long y)
{
x=(x+mod)%mod;
unsigned long long h=1;
while(y>0)
{
if(y&1)h=mul(h,x);
x=mul(x,x),y/=2;
}
return h;
}
void poi()
{
unsigned long long x,y;
cin>>x>>y;
if(x==1)
{
printf("1\n");
return;
}
mod=x;
cout<<(poww((4*mod+1-4*y)%mod,(mod-1)/2)+mod)%mod<<'\n';
return;
}
int main()
{
unsigned long long qwe;
cin>>qwe;
for(int i=1;i<=qwe;i++)poi();
return 0;
}
动态规划
【UVA1626】 Brackets sequence
题目描述
给定你一个长度小于等于 \(100\) 的字符串 \(S\) ,\(S\) 内只有 \(()[]\) 四种字符,添加最小的括号将 \(S\) 变成合法的括号匹配序列,求变化后的 \(S\) 。
解题思路
多种括号的括号匹配不好做,数据较小,考虑区间 \(dp\) 。
设 \(f_{i,j}\) 表示区间 \([i,j]\) 内的字符补成合法序列的最小步数,我们可以很好想到转移 \(f_{i,j}=f_{i,u}+f_{u+1,j}\) 。
注意两端括号能组合的可以不拆,即 \(f_{i,j}=f_{i+1,j-1}\) 。
还要输出方案,记录转移,最后递归输出即可。
时间复杂度 \(O(n^3)\) 。
【CF1781F】 Bracket Insertion
题目描述
Feyn 喜欢玩括号序列。今天他想执行如下步骤 \(n\) 次来构建一个括号序列:
-
等概率随机选择一个空位(若当前有 \(k\) 个字符,则有 \(k+1\) 个空位)。
-
以 \(p\) 的概率插入字符串
()
或以 \(1-p\) 的概率插入字符串)(
,操作后字符串长度增加 \(2\)。
给定 \(n,p\),求出 Feyn 得到一个合法括号序列的概率,对 \(998244353\) 取模 \(1 \le n \le 500,1 \le q \le 10^4\)。
解题思路
只有单种括号,将 \((\) 转化为 \(+1\) ,\()\) 转化为 \(-1\) ,用前缀和处理。
分析题目,发现每次就是把序列中的一个数 \(x\) ,变为 \(x,x+1,x\) 或 \(x,x-1,x\) ,两者的概率分别为 \(p\) 与 \(1-p\) ,求每个数都大于等于 \(0\) 的概率为多少。
我们可以想到一个很重要的性质:对于每个数,他所在的位置不重要,那这个问题就可以不放在序列里了,我们可以把它放在一个可重集里。
想到一个 \(dp\) :设 \(f_{i,j}\) 表示做到某个数时集合里有 \(i\) 个数 \(j\) 的概率,但由于状态之间关系不大,难以转移。
再次尝试转化问题,其实问题就是开始给一个数 \(0\) ,对于一个数 \(x\) ,每次有 \(p\) 的概率将其转化为 \(x,x+1,x\) ,有 \(1-p\) 的概率将其转化为 \(x,x-1,x\) 。
我们可以将每次转化的三个数分割开来看,他们内部的转化是不会互相影响的,也就是说它们是独立的。
根据这个性质,我们就可以倒着做 \(dp\) 了,设状态 \(f_{i,j}\) 为对于数 \(j\) 还可以转化 \(i\) 次不出界的概率为多少。
由于概率不好求,我们转化为求方案数,一共 \(1 \times 3 \times ... \times (2n-1)\) 种方案,最后除掉即可。
\(f_{i,j}\) 由 \(f_{i-1,j},f_{i-1,j+1},f_{i-1,j-1}\) 转移而来,每次就是枚举分配给 \(j,j-1,j+1\) 的转化次数为多少,具体转移为:
初始状态为 \(f_{0,i}=1\) ,答案为 \(f_{n,0}\) 。
这个 \(dp\) 的时间复杂度为 \(O(n^4)\) ,会超时,我们观察到,每次有两个 \(f_{x,j},f_{y,j}\) ,这个东西我们可以预处理。
设 \(d_{i,j}=\sum_{x=0}^i C_{i}^x \times f_{x,j} f_{i-x,j}\) ,每次 \(dp\) 一层以后处理一下这个,状态转移方程变为:
时间复杂度就降到 \(O(n^3)\) 了。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,p,f[505][505],d[505][505],C[505][505];
long long poww(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
int main()
{
long long x,y;
scanf("%lld%lld",&n,&p),p=(p*poww(10000,mod-2))%mod;
for(int i=0;i<=500;i++)C[i][0]=C[i][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
for(int i=0;i<=n;i++)f[0][i]=1,d[0][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=n;j++)
{
x=y=0;
for(int u=0;u<=i-1;u++)x=(x+(d[u][j]*C[i-1][u]%mod)*f[i-1-u][j+1]%mod)%mod;
if(j!=0)for(int u=0;u<=i-1;u++)y=(y+(d[u][j]*C[i-1][u]%mod)*f[i-1-u][j-1]%mod)%mod;
f[i][j]=(x*p%mod+y*(mod+1-p)%mod)%mod;
}
for(int j=0;j<=n;j++)
{
for(int u=i;u<=n;u++)if(i>=u-i)d[u][j]=(d[u][j]+(((i==u-i)?1:2)*(C[u][i]*f[i][j]%mod)*f[u-i][j]%mod)%mod)%mod;
}
}
for(int i=1;i<=2*n;i+=2)f[n][0]=(f[n][0]*poww(i,mod-2))%mod;
cout<<f[n][0];
return 0;
}
【Luogu P4363】 一双木棋
题目描述
菲菲和牛牛在一块 \(n\) 行 \(m\) 列的棋盘上下棋,菲菲执黑棋先手,牛牛执白棋后手。
棋局开始时,棋盘上没有任何棋子,两人轮流在格子上落子,直到填满棋盘时结束。
落子的规则是:一个格子可以落子当且仅当这个格子内没有棋子且这个格子的左侧及上方的所有格子内都有棋子。
棋盘的每个格子上,都写有两个非负整数,从上到下第 \(i\) 行中从左到右第 \(j\) 列的格子上的两个整数记作 \(a_{i,j}\) 和 \(b_{i,j}\)。
在游戏结束后,菲菲和牛牛会分别计算自己的得分:菲菲的得分是所有有黑棋的格子上的 \(a_{i,j}\) 之和,牛牛的得分是所有有白棋的格子上的 \(b_{i,j}\) 的和。
菲菲和牛牛都希望,自己的得分减去对方的得分得到的结果最大。现在他们想知道,在给定的棋盘上,如果双方都采用最优策略且知道对方会采用最优策略,那么,最终牛牛会比菲菲高多少?
\(1 \le n,m \le 10^5\)
解题思路
首先,注意到只能在左边与上面都有棋子的格子下棋,这告诉我们,每下完一步,每列的棋子一定是全部在上面的,且棋子数随着行数增大而减小。
数据范围极小,再加上轮廓线规律的特短,我们可以将轮廓线给状压出来。
将 \(-\) 用 \(0\) 表示,\(|\) 用 \(1\) 表示,状态个数有 \(2^{n+m}\) 个,实际没有这么多。
注意到这道题是一道博弈论,我们要倒着 \(dp\) ,考虑牛牛每一步菲菲的最优走法,然后转移。
时间复杂度 \(O(C_{n+m}^n n^2)\) 。
注意一个点,\(nm\) 有可能为奇数,代表最后一步是由牛牛下的。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
int x,y;
};
int n,m,f[1200005],v1[15][15],v2[15][15],st,en;
vector<int> t[1200005];
vector<datay> t1[1200005];
int main()
{
memset(f,0x80,sizeof(f));
scanf("%d%d",&n,&m);
int p=1<<(n+m),x=0,y=0,z=0;datay q;
for(int i=0;i<p;i++)
{
x=0;
for(int j=0;j<n+m;j++)if(i&(1<<j))x++;
if(x!=n)continue;
q.x=1,q.y=m+1;
for(int j=0;j<n+m;j++)
{
if(i&(1<<j))q.x++;
else q.y--;
if((!(i&(1<<j)))&&(i&(1<<(j+1))))t[i].push_back((i^(1<<j))^(1<<(j+1))),t1[i].push_back(q);
}
}
for(int i=0;i<n;i++)st+=(1<<i);
for(int i=0;i<n;i++)en+=(1<<(i+m));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)scanf("%d",&v1[i][j]);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)scanf("%d",&v2[i][j]);
}
f[st]=0;
for(int i=0;i<p;i++)
{
if(t[i].size()==0)continue;
for(int j=0;j<t[i].size();j++)
{
x=t[i][j],z=1e9;
for(int u=0;u<t[x].size();u++)z=min(z,f[t[x][u]]-v2[t1[x][u].x][t1[x][u].y]);
if(z==1e9)f[i]=f[t[i][j]]+v1[t1[i][j].x][t1[i][j].y];
else f[i]=max(f[i],v1[t1[i][j].x][t1[i][j].y]+z);
}
}
cout<<f[en];
return 0;
}
【Luogu P5369】 最大前缀和
题目描述
小 C 是一个算法竞赛爱好者,有一天小 C 遇到了一个非常难的问题:求一个序列的最大子段和。
但是小 C 并不会做这个题,于是小 C 决定把序列随机打乱,然后取序列的最大前缀和作为答案。
小 C 是一个非常有自知之明的人,他知道自己的算法完全不对,所以并不关心正确率,他只关心求出的解的期望值,现在请你帮他解决这个问题,由于答案可能非常复杂,所以你只需要输出答案乘上 \(n!\) 后对 \(998244353\) 取模的值,显然这是个整数。
\(1 \le n \le 20\) 。
解题思路
看到题我们很容易想到状压 \(dp\) 。
设全集为 \(U\) ,我们考虑求出使得集合 \(S\) 里面的数排成的序列的最大前缀和在最后一位的方案数 \(f_S\),然后求出补集的最大前缀和小于 \(0\) 的方案数 \(g_{S-U}\),两者相乘即可。
但是转移 \(f_S\) 的时间复杂度起步是 \(O(S^2)\) 即为 \(O(2^{2n})\) 的,我们肯定不能接受。
我们一个序列前缀和的形状,可以发现,\(f_S\) 就是从最后一位向前看,最大前缀和小于 \(0\) 的方案数,它接近 \(g_{S}\) 反过来的含义。
对于 \(f_S\) 与 \(g_S\) 我们都可以 \(O(2^nn)\) 解决掉,最后相乘即可。
注意最大前缀和可能有多个,这就是 \(f_S\) 与 \(g_S\) 不一样的地方,\(f_S\) 不包含重复的最大前缀和,\(g_S\) 包含。
时间复杂度 \(O(2^nn)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,a[25],f[2400005],t[2400005],d[2400005],s;
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
long long p=(1<<n);
f[0]=d[0]=1;
for(int i=1;i<p;i++)
{
for(int j=1;j<=n;j++)if(i&(1<<(j-1)))t[i]+=a[j];
}
for(int i=0;i<p-1;i++)
{
if(t[i]<0)continue;
for(int j=1;j<=n;j++)if(!(i&(1<<(j-1))))f[i^(1<<(j-1))]=(f[i^(1<<(j-1))]+f[i])%mod;
}
for(int i=1;i<p;i++)
{
if(t[i]>=0)continue;
for(int j=1;j<=n;j++)if(i&(1<<(j-1)))d[i]=(d[i]+d[i^(1<<(j-1))])%mod;
}
for(int i=1;i<p;i++)
{
if(!(i&(i-1)))s=(s+((t[i]%mod)*d[(p-1)^i])%mod)%mod;
else s=(s+(((t[i]%mod)*f[i]%mod)*d[(p-1)^i])%mod)%mod;
}
cout<<(s+mod)%mod;
return 0;
}