ARC C 题做题记录
最后更新时间
2023/12/26
温馨提示
右下角有展开目录索引的功能。
由于是做题记录,所以作为题解的话可能有些地方的表达不是特别好,请见谅。
[ARC104C] Fair Elevator
\(2n\) 个位置,要不重复地填 \(2n\) 个数,所以可以发现题目中给的区间里面带 \(-1\) 的都是没有用的,因为反正最后肯定要填,所以它不能提供任何信息。也就是说只有完整的区间是有用的区间信息,但是给出的端点信息也是有用的,因为它确定了某个位置只能作为左端点或者右端点。
首先在输入的时候排除一些显然无解的情况,然后接下来有两种做法:
-
发现题目中的限制条件意味着如果一些区间它们有交,那么会形成形如 \([x,y],[x+1,y+1],[x+2,y+2]\dots\) 这样的形式,那么我们考虑假设我们现在要加一个区间 \([l,r]\),怎样判断它加进去是否合法。我们枚举 \(i\in [l,r)\),如果 \(i\) 已经被作为了题目中给出的完整区间的左端点,并且它的右端点不是 \([i+r-l]\)(即违反了我们上面提到的那种形式),就不合法,\(i\) 作为右端点的情况类似。还有就是如果不能存在 \([i,i+r-l]\) 这个区间也是不行的,即 \(i\) 和 \(i+r-l\) 都已经被定为端点了,但它们没有匹配。于是可以 \(O(n^3)\) 暴力处理出每个区间是否合法,然后再 \(O(n^3)\) 暴力做区间 \(dp\),枚举断点看是否能分成左右两个合法区间,答案即是 \(f_{1,2n}\)。
代码参考这篇题解的实现,懒得写了。 -
考虑判断一段连续的位置能否被合法地填满,这里只考虑填上文提到的那种相交的形式,因为如果不相交的话显然可以拆开,互不影响。假设我们要填的连续段为 \([l,r]\),记 \(len=r-l+1\),显然此处 \(len\) 应为偶数(保证每个区间左右端点配对),并且里面填的每个区间的长度应为 \(\frac{len}{2}+1\)(想象一下或者动手画一下就明白了)。那么考虑双指针 \(i,j\) 扫,枚举往里面填的区间的左右端点,如果 \(i\) 被确定为了右端点或者 \(j\) 被确定为了左端点或者 \(i,j\) 都被确定了但它们没有配对,那么就是不合法的。扫完之后若仍然合法则合法。然后就可以 \(dp\) 了,记 \(f_i\) 表示是否能合法地填完前缀 \(i\)。转移就枚举前面的 \(j\),如果存在 \(f_j\land Check(j+1,i)\),那么 \(f_i=true\)。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=205;
int n,a[N],b[N],p[N];
bool f[N];
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
il bool Calc(int l,int r){
int len=(r-l+1)>>1;
for(re int i=l,j=l+len;j<=r;i++,j++){
if(p[i]&&p[j]&&p[i]^p[j])return 0;
if(p[i]&&b[p[i]]==i)return 0;
if(p[j]&&a[p[j]]==j)return 0;
}
return 1;
}
int main(){
n=read();
for(re int i=1;i<=n;i++){
a[i]=read(),b[i]=read();
if(a[i]!=-1){
if(p[a[i]])return puts("No"),0;
else p[a[i]]=i;
}
if(b[i]!=-1){
if(p[b[i]])return puts("No"),0;
else p[b[i]]=i;
}
}
f[0]=1;
for(re int i=2;i<=(n<<1);i++)
for(re int j=i-2;j>=0;j-=2)
if(f[j]&&Calc(j+1,i)){
f[i]=1;
break;
}
puts(f[n<<1]?"Yes":"No");
return 0;
}
[ARC105C] Camels and Bridge
数据范围提示我们枚举全排列,然后考虑计算一个排列的答案,可以对一段连续的骆驼去 \(check\) 它们在一起通过桥的话至少应该有多少长度,记这些骆驼的总重为 \(sum\),那么显然只有限重 \(\lt sum\) 的桥会产生影响,影响的结果是每个这样的桥的长度都要对这一段骆驼的最短长度贡献一个 \(\max\),然后发现这就是一个前缀 \(\max\) 的形式,那么把桥按照限重从大到小排序,排完序之后处理处前缀 \(\max\),每次我们计算时直接把 \(sum\) 丢进去 \(\text{lower_bound}\) 即可。
然后就可以 \(dp\) 了,\(f_i\) 表示考虑前 \(i\) 个骆驼的最短长度,\(f_i=max_{j=1}^{i-1}\{f_j+Calc(j,i)\}\)。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define re register
const int N=10,M=1e5+5;
int n,m,a[N],ans=1e18,l[M],v[M],s[N],f[N];
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
struct bridge{
int l,v;
}b[M];
il bool cmp(bridge x,bridge y){
return x.v<y.v;
}
il void check(){
memset(f,0,sizeof f);
for(re int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
for(re int i=2;i<=n;i++)
for(re int j=1;j<i;j++)
f[i]=max(f[i],f[j]+l[lower_bound(v+1,v+1+m,s[i]-s[j-1])-v-1]);
ans=min(ans,f[n]);
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
n=read(),m=read();
int mx=0,mn=1e9;
for(re int i=1;i<=n;i++)a[i]=read(),mx=max(mx,a[i]);
for(re int i=1;i<=m;i++)b[i].l=read(),b[i].v=read(),mn=min(mn,b[i].v);
if(mx>mn)return puts("-1"),0;
sort(b+1,b+1+m,cmp);
for(re int i=1;i<=m;i++)l[i]=b[i].l,v[i]=b[i].v;
for(re int i=1;i<=m;i++)l[i]=max(l[i],l[i-1]);
sort(a+1,a+1+n);
do{
check();
}while(next_permutation(a+1,a+1+n));
cout<<ans;
return 0;
}
[ARC106C] Solutions
先判掉一车无解:
-
\(m\lt 0\) 无解,因为 \(A\) 是正解贪心。
-
\(m=n\) 无解,因为 \(B\) 至少会选一个。
-
\(m=n-1\),说明 \(ansB=1,ansA=n\),此时若 \(n\neq 1\),说明这 \(n\) 条线段互不相交,那么 \(ansB\) 也应为 \(n\),矛盾,所以 \(m=n-1\land n\neq 1\) 时无解。
然后考虑什么时候 \(A\) 会少选。即是按左端点排序之后选到了一个左端点小但右端点巨大的线段,导致它包含了很多线段,然后都不能选了,但是 \(B\) 就可以不选这个线段,去选里面的更多的线段。
于是就显然了。先放一条巨长的线段,再在这个线段里面放 \(m+1\) 条互不相交的小线段,这样 \(A\) 会选到里面的 \(m+1\) 条线段,\(B\) 只会选到这条很长的线段,然后就满足题意了。最后在外面补上 \(n-m-2\) 条互不相交的线段即可。
注意特判 \(m=0\),直接放 \(n\) 条互不相交的线段。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=2e5,star=5e5;
int n,m;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
int main(){
n=read(),m=read();
if(m<0||m==n||(m==n-1&&n>=2))return puts("-1"),0;
if(!m){
for(re int i=1;i<=n;i++)cout<<(i<<1|1)<<' '<<((i+1)<<1)<<'\n';
return 0;
}
cout<<1<<' '<<star<<'\n';
for(re int i=1;i<=m+1;i++)cout<<(i<<1|1)<<' '<<((i+1)<<1)<<'\n';
for(re int i=1;i<=n-m-2;i++)cout<<(i<<1|1)+star<<' '<<((i+1)<<1)+star<<'\n';
return 0;
}
[ARC107C] Shuffle Permutation
\(\text{swap}\) 行列不会改变每一行以及每一列内的元素集,只会打乱行列之间的相对顺序,所以答案为行可以 \(\text{swap}\) 出的种类数和列可有 \(\text{swap}\) 出的种类数相乘。
\(\text{swap}\) 有传递性,即如果 \(a\) 能 \(\text{swap}\) \(b\),\(b\) 能 \(\text{swap}\) \(c\),那么 \(a,b,c\) 就可以一起任意 \(\text{swap}\),那么并查集维护可以放在一起 \(\text{swap}\) 的行,\(O(n^2)\) 枚举两行是否可以 \(\text{swap}\),\(O(n)\) \(\text{check}\),如果可以就把这两行合并。最后得到的行的种类数就是并查集中每个集合的 \(siz\) 的阶乘再乘起来。列的计算是一样的。时间复杂度 \(O(n^3)\)。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define re register
const int N=55,mod=998244353;
int n,a[N][N],k,fa[N],siz[N],ans=1,fac[N];
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
il int fnd(int x){
return (fa[x]==x)?x:(fa[x]=fnd(fa[x]));
}
il void Merge(int x,int y){
x=fnd(x),y=fnd(y);
if(x==y)return;
if(siz[x]<siz[y])swap(x,y);
siz[x]+=siz[y],fa[y]=x;
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
n=read(),k=read();
for(re int i=1;i<=n;i++)
for(re int j=1;j<=n;j++)
a[i][j]=read();
fac[0]=1;
for(re int i=1;i<=n;i++)
fa[i]=i,siz[i]=1,fac[i]=fac[i-1]*i%mod;
for(re int i=1;i<n;i++)
for(re int j=i+1;j<=n;j++){
bool flg=1;
for(re int x=1;x<=n;x++)
if(a[x][i]+a[x][j]>k){
flg=0;
break;
}
if(flg)Merge(i,j);
}
for(re int i=1;i<=n;i++)
if(fa[i]==i)ans=ans*fac[siz[i]]%mod;
for(re int i=1;i<=n;i++)fa[i]=i,siz[i]=1;
for(re int i=1;i<n;i++)
for(re int j=i+1;j<=n;j++){
bool flg=1;
for(re int x=1;x<=n;x++)
if(a[i][x]+a[j][x]>k){
flg=0;
break;
}
if(flg)Merge(i,j);
}
for(re int i=1;i<=n;i++)
if(fa[i]==i)ans=ans*fac[siz[i]]%mod;
cout<<ans;
return 0;
}
[ARC108C] Keep Graph Connected
不知道为什么看到边与点之间的染色,我想到了树剖中把边的信息继承给点的方式,即继承给下面那个点。
那么我们尝试利用这个思想,随便跑一棵生成树出来,然后搜这棵生成树,把每条边下面的点染成边的颜色。
然后发现有时候一旦出现上下挨着的两条边颜色相同就 G 了。考虑把这两个相同的颜色断开,即把中间那个点染成另一个颜色,然后把上下两个点染成边的颜色,这样就好了。那么我们改进一下原先的策略:还是搜生成树,然后如果父节点已经染了和边相同的颜色那么我们就给这个点随便染一个其他的颜色,否则就染成边的颜色。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=2e5+5;
int n,m,idx,head[N],col[N];
bitset<N>vis;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
struct edge{
int u,v,w,nxt;
}e[N<<1];
vector<int>E[N];
il void adde(int u,int v,int w){
e[++idx]={u,v,w,head[u]};
head[u]=idx;
}
queue<int>q;
il void bfs(){
q.push(1),vis.set(1);
while(!q.empty()){
int u=q.front();
q.pop();
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(vis.test(v))continue;
if(col[u]==w)col[v]=(w==n)?w-1:w+1;
else col[v]=w;
vis.set(v),q.push(v);
}
}
}
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
n=read(),m=read();
for(re int i=1;i<=m;i++){
int u=read(),v=read(),w=read();
adde(u,v,w),adde(v,u,w);
}
col[1]=1,bfs();
for(re int i=1;i<=n;i++)cout<<col[i]<<'\n';
return 0;
}
[ARC109C] Large RPS Tournament
有 \(2^k\) 个位置,但本质不同的位置只有 \(n\) 个。所以直接 \(dp\),\(f_{i,j}\) 表示 \([i,i+2^j]\) 的胜出者。
转移显然,直接从中间断开就好了,注意这里的 \(i\) 是模 \(n\) 意义下的位置。
每次转移算出来的断点比较随机,所以上记搜。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=110;
int n,k,pow2[N];
string a;
char f[N][N];
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
il char calc(char a,char b){
if(a=='R')return b=='S'?a:b;
if(a=='P')return b=='R'?a:b;
return b=='P'?a:b;
}
il char dfs(int now,int i){
if(i==0)return f[now][i]=a[now];
if(f[now][i])return f[now][i];
return f[now][i]=calc(dfs(now,i-1),dfs((now+pow2[i-1])%n,i-1));
}
int main(){
n=read(),k=read();
cin>>a;
pow2[0]=1%n;
for(re int i=1;i<=k;i++)pow2[i]=(pow2[i-1]<<1)%n;
putchar(dfs(0,k));
return 0;
}
[ARC110C] Exoswap
由于每组相邻位置只能交换一次的限制,可以推出,如果要成功地使排列升序,任意时刻,\(a_i\gt i\land a_{i+1}\lt i+1\) 是 \(i,i+1\) 这对位置将会产生交换的充要条件。所以根据这个,我们可以确定下来一个排列变为升序的唯一确定的操作次数,或是无解。
把满足交换条件的 \(i\) 丢在一个集合里面,每次取出集合中的一个 \(i\),操作 \(i\),然后 \(\text{check}\) \(i-1\) 和 \(i+1\) 是否满足条件,是的话就丢进集合。
可以用队列实现。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=2e5+5;
int n,a[N],ans[N],cnt;
queue<int>q;
bitset<N>vis;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
il void check(int i){
if(a[i]>i&&a[i+1]<i+1)q.push(i);
}
int main(){
n=read();
for(re int i=1;i<=n;i++)a[i]=read();
for(re int i=1;i<=n;i++)check(i);
while(!q.empty()){
int x=q.front();
q.pop();
if(vis.test(x))continue;
vis.set(x),swap(a[x],a[x+1]),ans[++cnt]=x;
check(x-1),check(x+1);
}
if(cnt!=n-1)return puts("-1"),0;
for(re int i=1;i<=n;i++)if(a[i]^i)return puts("-1"),0;
for(re int i=1;i<n;i++)cout<<ans[i]<<'\n';
return 0;
}
[ARC111C] Too Heavy
先判掉显然无解的情况,即 \(\exists i,p_i\neq i\land b_{p_i}\ge a_i\),就是说存在一个人需要交换但交换不了。
交换两个数给排列排序,考虑置换环。对于每个置换环,在环内找到 \(a\) 最大的点,记为 \(k\),然后把环内其他每个点依次与 \(k\) 交换即可。
会不会出现中途换不了的情况?如果出现即当有一个要交换的点被限制不能交换,或者 \(k\) 被限制不能交换,前者已经判过无解了,后者情况的出现代表着 \(k\) 拿到了一个 \(\ge a_k\) 的 \(b\),而根据 \(k\) 的定义是 \(a\) 最大的点,那么 \(b\) 的来源点 \(i\) 的 \(a_i\) 值一定不会比 \(a_k\) 大,那么一定也有 \(a_i\le b_{p_i}\),也判过无解了。
所以正确。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=2e5+5;
int n,a[N],b[N],p[N];
#define pii pair<int,int>
#define mkp make_pair
pii ans[N];
int idx;
bitset<N>vis;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
int main(){
n=read();
for(re int i=1;i<=n;i++)a[i]=read();
for(re int i=1;i<=n;i++)b[i]=read();
for(re int i=1;i<=n;i++)p[i]=read();
for(re int i=1;i<=n;i++)
if(p[i]^i&&b[p[i]]>=a[i])
return puts("-1"),0;
for(re int i=1;i<=n;i++){
if(vis.test(i))continue;
int mx=0;
for(re int j=i;!vis.test(j);j=p[j])
vis.set(j),mx=(a[mx]>a[j])?mx:j;
for(re int j=mx;p[j]^mx;j=p[j])ans[++idx]=mkp(p[j],mx);
}
cout<<idx<<'\n';
for(re int i=1;i<=idx;i++)cout<<ans[i].first<<' '<<ans[i].second<<'\n';
return 0;
}
[ARC112C] DFS Game
最开始想法是把叶子结点按深度从小到大排序,每次给对手选深度小的点。但是假得很直接,甚至计算答案的地方都写错了。
考虑到如果我们要决策某个点,就必须决策完所有子树才能确定策略,由此依赖性考虑树形 \(dp\)。
记 \(f\) 表示在子树中先手能够获得的最大分数,\(siz\) 为子树大小,\(g=siz-f\),即后手的答案。
手摸发现先后手根据子树大小的奇偶性来确定博弈完子树后是否会交换先后手。那么记当前节点为 \(u\),其子节点为 \(v\),先手拿完 \(u\) 点的贡献之后由后手开始决策选择进入哪个子树,考虑按 \(siz_v\) 的奇偶性分讨:
-
\(siz_v\) 为奇数(记为第一类点),意味着博弈完子树 \(v\) 之后会交换先后手,那么后手会在这类节点中尽量选择更优秀,能让他获得比先手金币更多,即 \(g_v-f_v\) 最大的点。
-
\(siz_v\) 为偶数
-
\(g_v\gt f_v\)(记为第二类点),那么后手肯定优先选这个 \(v\),因为选了之后既能赚到又能继续获得决策权。
-
\(g_v\le f_v\)(记为第三类点),那么后手选这个肯定亏,所以尽量不选。
-
所以后手的策略是先选第二类点,再在第一类的点里面按优劣顺序选,最后选第二类点,由于第一类点会交换先后手,所以要根据第一类点数量的奇偶性判断第二类点的贡献给先手还是后手。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=1e5+10;
int n,idx,head[N],fa[N];
int siz[N],f[N];
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
struct edge{
int u,v,nxt;
}e[N];
il void adde(int u,int v){
e[++idx]={u,v,head[u]};
head[u]=idx;
}
#define pii pair<int,int>
#define mkp make_pair
priority_queue<pii>q;
il void dfs(int x){
siz[x]=f[x]=1;
int tot=0,cnt=0;
for(re int i=head[x];i;i=e[i].nxt){
int v=e[i].v;
dfs(v),siz[x]+=siz[v],tot^=siz[v]&1;
}
for(re int i=head[x];i;i=e[i].nxt){
int v=e[i].v;
if(siz[v]&1)q.push(mkp(siz[v]-f[v]-f[v],f[v]));
else if(f[v]<siz[v]-f[v]||(f[v]>siz[v]-f[v]&&!tot))f[x]+=f[v];
else f[x]+=siz[v]-f[v];
}
while(!q.empty()){
f[x]+=q.top().second;
if(!(++cnt&1))f[x]+=q.top().first;
q.pop();
}
}
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
n=read();
for(re int i=2;i<=n;i++)adde(fa[i]=read(),i);
dfs(1);
cout<<f[1];
return 0;
}
[ARC113C] String Invasion
如果有一对相邻且相同的位置,那么就可以一直推平到末尾,贪心地从后面往前面倒着找这种位置,避免推平之后把这种位置推没了。注意要减去中间相同字符的数量。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define re register
const int N=2e5+10;
int n,cnt[26][N];
ll ans;
string s;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
int main(){
getline(cin,s),n=s.size(),s=' '+s;
for(re int i=1;i<=n;i++){
for(re int j=0;j<26;j++)cnt[j][i]=cnt[j][i-1];
cnt[s[i]-'a'][i]++;
}
for(re int i=n,lst=0;i;i--){
if(s[i]^s[i+1])continue;
if(lst==i+1){
lst=i;
continue;
}
int l=i+2,r=(s[lst]==s[i])?lst-1:n;
ans+=r-l+1-cnt[s[i]-'a'][lst?lst-1:n]+cnt[s[i]-'a'][l-1];
lst=i;
}
cout<<ans;
return 0;
}
[ARC114C] Sequence Scores
我最开始的思路是,记 \(pre_i\) 为上一个与 \(i\) 位置的数相等的位置,当 \(\min_{j=i}^{pre_i} \{a_j\}\ge a_i\) 时可以合并一次操作,最后的答案是 \(n\) 减去合并次数。
但是发现这样根本 \(dp\) 不了,每一时刻需要知道的信息很多,难以高效地表示。
于是看题解发现了一种很神奇的思路:
考虑操作 \((l,r,k)\) 什么时候会进行,就是 \(\min_{i=l}^r\{a_i\}=k\),且这样的 \([l,r]\) 是已经扩展到极长了的。即此时会出现 \((l=1\lor a_{l-1}\lt k)\land(r=n\lor a_{r+1}\lt k)\)。
对于 \(\min_{i=l}^r\{a_i\}=k\),计算方式是 \(\gt k\) 减去 \(\ge k\),即 \(Calc(l,r,k)=(m-k-1)^{r-l+1}-(m-k)^{r-l+1}\),然后我们发现这个 \(Calc\) 里面 \(l,r\) 起到的作用都只是区间长度 \(r-l+1\),所以可以写成 \(Calc(len,k)=(m-k-1)^{len}-(m-k)^{len}\)。
考虑到这个条件两边都有边界需要处理,所以直接按边界分类讨论,省去麻烦:
-
\(l=1,r=n\) 时,答案即为 \(Calc(n,k)\)。
-
\(l=1,r\neq n\) 时,此时需要满足 \(a_{r+1}\lt k\),剩下 \((r+1,n]\) 随便填,答案即为 \((n-len-1)^m (k-1) Calc(len,k)\)
-
\(l\neq 1,r=n\) 时,与上一种情况对称,答案一样。
-
\(l\neq 1,r\neq n\),即一般情况,\(a_{l-1}\) 和 \(a_{r+1}\) 都要 \(\lt k\),答案为 \((n-len-2)^m (k+1)^2 Calc(len,k)\)
预处理出所有求幂的结果,即可 \(O(1)\) 计算 \(Calc\)。
第一种情况是唯一的,\(O(1)\);第二三种情况有 \(O(n)\) 种;最后一种情况,也是发现答案的计算中只是 \(len\) 起到了作用,\(l,r\) 没有作用,所以枚举 \(len\) 计算答案,再乘上 \(len\) 对应的区间数量 \(n-len-1\) 即可。\(O(n)\)
然后全都要再算上枚举 \(k\) 的 \(O(m)\),总时间复杂度为 \(O(nm)\)。
$\texttt{Code}$
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define re register
const int N=5010,mod=998244353;
int n,m,poW[N][N],ans;
il int read(){
re int x=0;re char c=getchar(),f=0;
while(c<'0'||c>'9') f|=(c=='-'),c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c&15),c=getchar();
return f?-x:x;
}
il int Pow(int a,int b){
return (b<1)?1:poW[a][b];
}
il int Calc(int len,int k){
return (Pow(m-k+1,len)-Pow(m-k,len)+mod)%mod;
}
signed main(){
n=read(),m=read();
for(re int i=1;i<=m;i++){
poW[i][0]=1;
for(re int j=1;j<=n;j++)
poW[i][j]=poW[i][j-1]*i%mod;
}
for(re int k=1;k<=m;k++)ans+=Calc(n,k);
ans%=mod;
for(re int len=1;len<n;len++)
for(re int k=1;k<=m;k++)
ans+=(k-1)*Calc(len,k)%mod*Pow(m,n-len-1)%mod<<1;
ans%=mod;
for(re int len=1;len<=n-2;len++)
for(re int k=1;k<=m;k++)
ans+=(k-1)*(k-1)%mod*Calc(len,k)%mod*Pow(m,n-len-2)%mod*(n-len-1)%mod;
cout<<ans%mod;
return 0;
}