A - Takahashikun, The Strider
问题就是要你求 \(ax\equiv 0 \pmod{360}\) 中 \(a\) 的最小值。
答案就是 \(a=\frac{360}{\gcd(x,360)}\)。
代码:
#include<iostream>
#include<cstdio>
using namespace std;
int x;
int gcd(int a,int b)
{
return b==0?a:gcd(b,a%b);
}
int main()
{
scanf("%d",&x);
printf("%d",360/gcd(x,360));
return 0;
}
B - Extension
考虑 DP,令 \(f_{i,j}\) 表示当前填了 \(i\) 行 \(j\) 列的方案数,因为先填行再填列和先填列再填行有重合部分,需要减去,转移为:
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=3005;
const int MOD=998244353;
int A,B,C,D;
long long dp[N][N];
long long dfs(int r,int c)
{
if(r<A||c<B) return 0;
if(dp[r][c]!=-1) return dp[r][c];
int res=0;
res=(res+dfs(r,c-1)*r%MOD)%MOD;
res=(res+dfs(r-1,c)*c%MOD)%MOD;
res=(res-dfs(r-1,c-1)*(r-1)%MOD*(c-1)%MOD+MOD)%MOD;
return dp[r][c]=res;
}
int main()
{
scanf("%d%d%d%d",&A,&B,&C,&D);
memset(dp,-1,sizeof(dp));
dp[A][B]=1;
printf("%lld",dfs(C,D));
return 0;
}
C - Shift
不妨将所有的根据 \(0\) 分成若干段,令 \(f_{i,j,k}\) 表示当前填的 \(i\) 段,用了 \(j\) 个 \(1\),用了 \(k\) 次操作,转移时枚举当前段填的 \(1\) 的个数。
\(a_i\) 为当前段 \(1\) 的个数。
直接实现为 \(O(n^4)\) 的,可以用前缀和瞎 jb 搞一下就是 \(O(n^3)\) 的了,代码是 \(O(n^4)\) 的。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=305;
const int MOD=998244353;
int n,m;
char s[N];
int a[N];
int sum[N];
long long dp[N][N][N];
int main()
{
scanf("%s%d",s+1,&m);
n=strlen(s+1);
s[++n]='0';
m=min(m,n);
int cnt=0;
for(int i=1;i<=n;i++)
if(s[i]=='0') cnt++;
else a[cnt+1]++;
for(int i=1;i<=cnt;i++)
sum[i]=sum[i-1]+a[i];
dp[0][0][0]=1;
for(int i=1;i<=cnt;i++)
for(int j=sum[i];j<=n-cnt;j++)
for(int k=0;k<=m;k++)
for(int p=0;p<=min(j,n-sum[i-1]-cnt);p++)
if(k>=max(p-a[i],0)) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-p][k-max(p-a[i],0)])%MOD;
long long ans=0;
for(int k=0;k<=m;k++)
ans=(ans+dp[cnt][n-cnt][k])%MOD;
printf("%lld",ans);
return 0;
}
D - Secret Passage
考虑一个分界点 \(i\),我们需要知道 \(i\) 前面经过若干次操作后是否能得出 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 到后面,还有 \(i\) 后面加入 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 后能够得到多少种不同的方案。
考虑计算 \(i\) 前面经过若干次操作后是否能得出 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 到后面,令 \(f_{i,c_0,c_1}\) 表示当前到第 \(i\) 个位置,是否可以取出 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 扔到后面。那么对于 \(f_{i,c_0,c_1}\),考虑它如何转移:
- 当前点可以不动;
- 当 \(s_{i+1}=0\) 时,我们可以将一个扔到后面的 \(1\) 换成 \(0\);
- 当 \(s_{i+1}=1\) 时同理;
- 当 \(s_{i+1}=0\) 或 \(s_{i+2}=0\) 时,我们可以将一个 \(0\) 扔到后面,另一个数去掉;
- 当 \(s_{i+1}=1\) 或 \(s_{i+2}=1\) 时同理。
对应的转移为:
考虑计算后面加入 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 后能够得到多少种不同的方案,令 \(g_{i,c_0,c_1}\) 表示 \(i\) 后面加入 \(c_0\) 个 \(0\) 和 \(c_1\) 个 \(1\) 后能够得到多少种不同的方案。
考虑将连续的一段 \(0\),如果在这段中插入 \(0\) 时我们强制它插在开头,那么就可以转移了:
记得需要将空串的情况去掉。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=305;
const int MOD=998244353;
int n;
char s[N];
long long dp[N][N][N];
bool f[N][N][N];
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
dp[n][0][0]=1;
for(int i=n;i>=1;i--)
for(int c0=0;c0<i;c0++)
for(int c1=0;c0+c1<i;c1++)
{
dp[i-1][c0][c1]=(dp[i-1][c0][c1]+dp[i][c0][c1])%MOD;
if(s[i]=='0') dp[i][c0][c1+1]=(dp[i][c0][c1+1]+dp[i][c0][c1])%MOD;
else if(s[i]=='1') dp[i][c0+1][c1]=(dp[i][c0+1][c1]+dp[i][c0][c1])%MOD;
}
f[0][0][0]=true;
for(int i=0;i<=n;i++)
for(int c0=0;c0<=i;c0++)
for(int c1=0;c0+c1<=i;c1++)
{
if(i+1<=n)
{
f[i+1][c0][c1]|=f[i][c0][c1];
if(s[i+1]=='0'&&c1>0) f[i+1][c0+1][c1-1]|=f[i][c0][c1];
if(s[i+1]=='1'&&c0>0) f[i+1][c0-1][c1+1]|=f[i][c0][c1];
}
if(i+2<=n)
{
if(s[i+1]=='0'||s[i+2]=='0') f[i+2][c0+1][c1]|=f[i][c0][c1];
if(s[i+1]=='1'||s[i+2]=='1') f[i+2][c0][c1+1]|=f[i][c0][c1];
}
}
f[n][0][0]=false;
long long ans=0;
for(int i=0;i<=n;i++)
for(int c0=0;c0<=i;c0++)
for(int c1=0;c0+c1<=i;c1++)
if(f[i][c0][c1]) ans=(ans+dp[i][c0][c1])%MOD;
printf("%lld",ans);
return 0;
}
E - Permutation Cover
考虑最后的序列肯定是若干个排列相交的情况,考虑如果有一个点同时被多个排列同时覆盖,我们只需要考虑其中的两个排列。
考虑两个排列相交的情况,将某个数放在相交部分,它对当前这个数的个数的贡献为 \(1\),否则的贡献为 \(2\)。
那么可以看出答案为无解的情况为
否则一定可以构造出方案,具体证明的话看官方题解去吧。
考虑新插入某些数,枚举当前插入数的长度,贪心地构造方案:
- 当 \(\max(a_i)\le 2\cdot \min(a_i)\) 时,直接贪心将字典序最小的加入目前的答案串。
- 当 \(\max(a_i)= 2\cdot \min(a_i)+1\) 时,需要满足所有的 \(\max(a_i)\) 的位置都在 \(\min(a_i)\) 之前。
- 当 \(\max(a_i)> 2\cdot \min(a_i)+1\) 时,无法构造方案。
代码:
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int K=105,N=1005;
int k,n;
int a[K];
vector<int>merge(const vector<int>&a,const vector<int>&b)
{
vector<int>res;
size_t i=0,j=0;
while(i<a.size()&&j<b.size())
{
if(a[i]<b[j]) res.push_back(a[i]),i++;
else res.push_back(b[j]),j++;
}
while(i<a.size())
res.push_back(a[i]),i++;
while(j<b.size())
res.push_back(b[j]),j++;
return res;
}
int ans[N],tot;
int b[K];
bool pos[K];
vector<int>add(int len)
{
for(int i=1;i<=k;i++)
pos[i]=false,b[i]=a[i];
vector<int>pre;
for(int i=tot,j=1;i>=1&&j<=k-len;i--,j++)
pre.push_back(ans[i]);
for(int u:pre)
pos[u]=true;
for(int i=1;i<=k;i++)
if(!pos[i]) b[i]--;
for(int i=1;i<=k;i++)
if(b[i]<0) return {k+1};
int Min=*min_element(b+1,b+k+1),Max=*max_element(b+1,b+k+1);
if(Min*2>=Max)
{
vector<int>res;
for(int i=1;i<=k;i++)
if(!pos[i]) res.push_back(i);
return res;
}
else if(Min*2+1==Max)
{
vector<int>x,y;
for(int i=1;i<=k;i++)
if(!pos[i]&&b[i]==Max) x.push_back(i);
for(int i=1;i<=k;i++)
if(!pos[i]&&b[i]==Min) x.push_back(i);
for(int i=1;i<=k;i++)
if(!pos[i]&&b[i]!=Min&&b[i]!=Max) y.push_back(i);
vector<int>res=merge(x,y);
pre.insert(pre.end(),res.begin(),res.end());
int L=0,R=(int)(pre.size())-1;
for(int i=0;i<pre.size();i++)
{
if(b[pre[i]]==Max) L=max(L,i);
if(b[pre[i]]==Min) R=min(R,i);
}
if(L<R) return res;
else return {k+1};
}
else return {k+1};
}
void solve()
{
vector<int>res={k+1};
for(int len=1;len<=k&&tot+len<=n;len++)
{
int d=k-len;
if(tot-d>=0)
{
vector<int>now=add(len);
res=min(res,now);
}
}
for(int u:res)
{
ans[++tot]=u;
a[u]--;
printf("%d ",u);
}
return;
}
int main()
{
scanf("%d",&k);
for(int i=1;i<=k;i++)
scanf("%d",&a[i]),n+=a[i];
int Min=*min_element(a+1,a+k+1),Max=*max_element(a+1,a+k+1);
if(2*Min+1<=Max)
{
printf("-1");
return 0;
}
while(tot<n)
solve();
return 0;
}
F - Forbidden Tournament
考虑将原图缩点,缩完点以后的图一定是一个链状 DAG。可以发现,最多只有一个强联通分量的点的个数大于等于 \(3\),其他最多只有一个点。
可以枚举强联通分量的个数,那么我们只需要求出点数等于 \(n-i\),度数小于等于 \(k-i\) 的强联通图的方案数,最后乘上 \(P_{n}^i\) 就是了。
考虑计算满足条件的强联通图的方案数,不妨选一个点 \(1\),将它的所有的入度和出度分为两个集合 \(X,Y\),可以发现 \(X,Y\) 都是链状 DAG。
不妨将 \(X,Y\) 中的点 \(x_1,x_2,\cdots ,x_n\) 和 \(y_1,y_2,\cdots ,y_m\) 按照拓扑序排序。显然必有 \(y_m\to x_1\) 的边。可以发现,从 \(y_b\) 连向 \(x_i\) 的边必然是一个前缀,且那个分界点具有单调性。
那么就可以将问题转化到网格上,大概是一个折线计数问题,还要考虑 \(k\) 的限制,大概就是两条斜线限制一下,将路径上方的点染成黑色,其他为白色。令 \(f_{i,j}\) 表示第 \(i\) 行有 \(j\) 个黑点的方案数,DP 一下即可。
代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=205;
int n,k,p;
long long ksm(long long a,long long b)
{
long long res=1;
while(b)
{
if(b&1) res=res*a%p;
a=a*a%p,b>>=1;
}
return res;
}
long long fac[N],inv[N];
void init(int n=200)
{
fac[0]=1;
for(int i=1;i<=n;i++)
fac[i]=fac[i-1]*i%p;
inv[n]=ksm(fac[n],p-2);
for(int i=n;i>=1;i--)
inv[i-1]=inv[i]*i%p;
return;
}
long long P(int n,int m)
{
if(m>n) return 0;
return fac[n]*inv[n-m]%p;
}
long long f[N][N];
long long sum[N][N];
int calc(int n,int m,int k)
{
f[0][m]=1;
for(int i=1;i<=m-1;i++)
f[0][i]=0;
f[0][0]=-1;
for(int j=0;j<=m;j++)
{
sum[0][j]=j>0?sum[0][j-1]:0;
sum[0][j]=(sum[0][j]+f[0][j]+p)%p;
}
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
f[i][j]=0;
if(i-1+j<=k&&n-i+m-j<=k)
{
f[i][j]=(f[i][j]+(sum[i-1][m]-(j>0?sum[i-1][j-1]:0)+p)%p)%p;
}
sum[i][j]=j>0?sum[i][j-1]:0;
sum[i][j]=(sum[i][j]+f[i][j])%p;
}
return f[n][0];
}
long long solve(int n,int k)
{
if(n==1) return 1;
long long res=0;
for(int deg=2;deg<n;deg++)
res=(res+calc(deg,n-deg,k))%p;
return res*fac[n-1]%p;
}
int main()
{
scanf("%d%d%d",&n,&k,&p);
init();
long long ans=0;
for(int i=0;i<=k;i++)
ans=(ans+P(n,i)*solve(n-i,k-i)%p)%p;
printf("%lld",ans);
return 0;
}
- AtCoder Contest Grand 046atcoder contest grand 046 authentic atcoder contest grand negative atcoder contest grand atcoder contest grand 022 atcoder contest grand 001 histogram atcoder contest grand atcoder contest radius grand atcoder contest descent grand atcoder contest grand 019 atcoder contest grand 017