坐电梯 2026.1
直接计数。图个数为 \(G=\sum\limits_{d|n}\frac{n!}{(d!)^c c!}(c=\frac{n}{d})\),即划分为 \(d\) 个集合,然后再钦定顺序(图是无序的)。
据欧拉定理答案就是 \(m^{G\bmod (P-1)+P-1}\)(\(P=10^9-401\) 是质数)。
把 \(P-1\) 分解成 \(2\times13\times5281\times7283\),对这四个 \(q\) 如下分别算 \(G\bmod q\) 最后 CRT 合并:
枚举 \(d\),把 \(q^{\nu(n!)-c\nu(d!)-\nu(c!)}\) 拆出来,用 exLucas 算 \((k!)_q\),这样就能求逆元,再把 \(q\) 乘回去。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=8e3+10,mod=1e9-401;
int T,n,m,fct[N],k,q,a[4],b[4];
vector<int>f;
int qpow(int x,int y,int P){
int ret=1;
while(y){
if(y&1)ret=1ll*ret*x%P;
y>>=1;x=1ll*x*x%P;
}
return ret;
}
int exgcd(int a,int b,int &x,int &y){
if(!b)return x=1,y=0,a;
int r=exgcd(b,a%b,x,y),t=x;
x=y;
y=t-a/b*y;
return r;
}
LL num(int n){
LL r=0;
while(n)r+=(n/=q);
return r;
}
int calc(int n){
if(!n)return 1;
return 1ll*calc(n/q)*(n/q%2?q-1:1)%q*fct[n%q]%q;
//此处q=q^c
}
void cjw(){
a[k]=q;
for(int i=1;i<q;i++)fct[i]=1ll*i*fct[i-1]%q;
int res=0,pw,now;
for(int i:f){
pw=num(n)-num(i)*(n/i)-num(n/i);
if(pw)continue;
now=1ll*calc(n)*qpow(1ll*qpow(calc(i),n/i,q)*calc(n/i)%q,q-2,q)%q;
res=(res+now)%q;
}
b[k]=res;
k++;
}
int crt(){
int A=1,B=0;
for(int i=0;i<k;i++){
int p,g=exgcd(A,a[i],p,*new int);
p=((b[i]-B)/g*p%(a[i]/g)+a[i]/g)%(a[i]/g);
B+=A*p;
A=A/g*a[i];
}
return B+A;
}
signed main(){
scanf("%d",&T);
fct[0]=1;
while(T--){
//mod-1=2*13*5281*7283
k=0;f.clear();
scanf("%d%d",&n,&m);
for(int i=1;i*i<=n;i++)if(n%i==0){
f.push_back(i);
if(i*i!=n)f.push_back(n/i);
}
q=2;cjw();
q=13;cjw();
q=5281;cjw();
q=7283;cjw();
printf("%d\n",qpow(m,crt(),mod));
}
return 0;
}
\(p,q\) 指定了上下两行的取值顺序,\(s\) 则指定了序列间的拓扑序。只要无环就存在 \(a\)。
考虑如果 \(p,q,s\) 都已知,三者同时按 \(p\) 重排没有影响(重标号不改变图形态)。具体考察环存在性:

只有 \(i<j,q_i>q_j,s_i=1,s_j=0\) 这一种情况,我们要求它不存在。
多维偏序,把 \((i,q_i)\) 画到平面上:

如果选中一个点为 \(s=1\),那么其右下方必须全为 \(1\)。因此,一个上升子序列可以唯一描述一个合法方案。一个合法方案也可以简单构造出一个上升子序列。故求所有 \(q\) 的上升子序列个数和。
设 \(f_{i,j,k}\) 表示前 \(i\) 个数,有 \(j\) 个 \(0\) 被钦定值并选入子序列,子序列最后一个数是 \(k\) 的方案数,前缀和优化可以简单求解。最后把未选入的位置数学统计下。由于要求是上升子序列,选中的 \(0\) 不会被钦定为相等值,只需令非 \(0\) 的确定位值不重复出现。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=5e2+10,mod=998244353;
int n,p[N],q[N],rq[N],ans,f[N][N][N],m,fct[N];
void add(int &x,int y){if((x+=y)>=mod)x-=mod;}
signed main(){
freopen("permutation.in","r",stdin);
freopen("permutation.out","w",stdout);
scanf("%d",&n);
fct[0]=1;
for(int i=1;i<=n;i++)scanf("%d",&p[i]),fct[i]=1ll*i*fct[i-1]%mod;
for(int i=1;i<=n;i++)scanf("%d",&q[p[i]]);
for(int i=1;i<=n;i++)rq[q[i]]=i,m+=!!q[i];
for(int i=0;i<=n;i++)f[0][0][i]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++)
for(int k=0;k<=n;k++){
if(k)f[i][j][k]=f[i][j][k-1];
add(f[i][j][k],(f[i-1][j][k]-(k?f[i-1][j][k-1]:0)+mod)%mod);
if(q[i]&&k==q[i])add(f[i][j][k],f[i-1][j][k-1]);
if(!q[i]&&j&&k&&!rq[k])add(f[i][j][k],f[i-1][j-1][k-1]);
}
for(int i=0;i<=n-m;i++)
add(ans,1ll*f[n][i][n]*fct[n-m-i]%mod);
printf("%d\n",ans);
return 0;
}
【已编辑】
吃完【菌菇酥肉火烧米线】不再发电。
如果我能走到知识较完备、水平冲省队的那一天,可以多看看各种人写的冷门技巧、自发推广。
解除 * 使用。
完全无法 dp,找结构。
考虑最大值 \(a\),如果存在 \(b\) 于答案中和 \(a\) 同向移动并且更远,把 \(a,b\) 终点互换更优。
如果 \(a\) 没有移动到答案两端,上述情况一定出现。所以策略是:将值从大到小依次放在最左侧或最右侧。
可以区间 dp 了。倒着来等于是从小到大自中间开始向两端放置,更方便。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=2e3+10;
int n;
LL f[N][N];
pair<int,int>a[N];
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i].fi),a[i].se=i;
sort(a+1,a+n+1);
for(int l=1;l<=n;l++){
for(int i=1;i+l-1<=n;i++){
int j=i+l-1;
f[i][j]=max(f[i+1][j]+1ll*a[l].fi*abs(i-a[l].se)
,f[i][j-1]+1ll*a[l].fi*abs(j-a[l].se));
}
}
printf("%lld\n",f[1][n]);
return 0;
}
由于做过 P9963 我直接往间隔长度去想,\(n^3\) 坠。
重新考虑代价计算的方式:初始 \(1\) 位置序列是 \(\{a_m\}\),操作成 \(\{b_m\}\) 的话,代价为 \(s=\sum |a_i-b_i|\)。这里是最小次数,简单发现该方案满足 \(k\ge s,2|k-s\) 就能做到恰好 \(k\) 次。
拆贡献。设 \(a\) 有 \(c_1\) 个 \(1\) 在 \([1,i]\) 中,\(b\) 有 \(c_2\) 个,那么 \(i\leftrightarrow i+1\) 这个间隙最终恰好被经过 \(|c_1-c_2|\) 次,同样可用于计算 \(s=\bf{\sum|\Delta|}\)。如果直接从左到右在 dp 中构造出最终状态,可以见得 \(|\Delta|\) 每次至多 \(\bf{+1}\)。
此处应有应激:\(|\Delta|\le2\sqrt{k}\)。
这样就能做了,记录 \(f_{i,s,\Delta}\) 容易转移。答案按上述统计。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1510,mod=1e9+7;
int n,m,a[N],f[2][N][120],s[N];
void add(int &x,int y){if((x+=y)>=mod)x-=mod;}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),s[i]=s[i-1]+a[i];
int M=sqrt(m*2);
f[0][0][M]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=-M;k<=M;k++){
if(j+abs(k+1-a[i])<=m&&-M<=k+1-a[i]&&k+1-a[i]<=M)
add(f[i&1][j+abs(k+1-a[i])][k+1-a[i]+M],f[i&1^1][j][k+M]);
if(j+abs(k-a[i])<=m&&-M<=k-a[i]&&k-a[i]<=M)
add(f[i&1][j+abs(k-a[i])][k-a[i]+M],f[i&1^1][j][k+M]);
f[i&1^1][j][k+M]=0;
}
int ans=0;
for(int i=m%2;i<=m;i+=2)add(ans,f[n&1][i][M]);
printf("%d\n",ans);
return 0;
}
场上过的很少,大概是因为这题可行的暴力状态很多。
这题兑很多人来说应该非常水,但是我注意了很久才注意到。
要求前缀最大值之和,那么不是前缀最大值的数很可能没有用。仔细考量:如果 \(a\) 中存在一个非严格逆序对 \(i<j,a_i>a_j\),把 \(b_j\) 全部加给 \(b_i\) 一定不劣。因此有值的一定是 \(a\) 序列中的严格前缀最大值位置。
就卡在这里了。想了一晚上才注意到:严格前缀最大值位置数是 \(\bf\min(V,n)\) 的,这题 \(V\le 360\)。
那直接 dp 了哈哈,做到 \(V^3\)。给 dp 数组一个前缀最大值优化就行了,绷。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e5+10,M=400;
int n,m,a[N],f[M][M][M],k,s[M][M][M];
pair<int,int>b[M];
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&k);
m=0;
for(int i=1,mx=0;i<=n;i++){
scanf("%d",&a[i]);
if(a[i]>mx)b[++m]={i,a[i]},mx=a[i];
}
b[m+1]={n+1,0};
for(int i=0;i<=k;i++)for(int j=0;j<=b[m].se;j++)f[0][i][j]=-1e9;
f[0][0][0]=0;
for(int i=0;i<=b[m].se;i++)s[0][0][i]=0;
for(int i=1;i<=m;i++){
for(int j=0;j<=k;j++)
for(int y=0;y<=b[m].se;y++){
if(y<=b[i].se)f[i][j][y]=max(j>=y&&y?s[i-1][j-y][y-1]:(int)-1e9,f[i-1][j][y])+(b[i+1].fi-b[i].fi)*y;
else f[i][j][y]=-1e9;
s[i][j][y]=max(y?s[i][j][y-1]:(int)-1e9,f[i][j][y]);
}
}
printf("%d\n",s[m][k][b[m].se]);
}
return 0;
}
非常经典,完全图直接用 boruvka。考虑求出关于一个点的最短边。
给出了树形态,考虑树形 dp。比较容易求出子树中与自己距离(题面中的边权)最近的点。换根也很简单,dp 时记录子节点转移值的前后缀 min 即可。
由于要求所在联通块不同,把 dp 值改为记录最近点、所在联通块不同于最近点的次近点。
然后就是正常 brvk 流程,复杂度 \(\Theta(n\log^2 n)\),因为有并查集。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=2e5+10;
int n,a[N],m,t[N],W[N];
vector<pair<int,int>>e[N];
struct node{LL p1,p2;int c1,c2;}f[N],g[N];
vector<node>l[N],r[N];
bool frst=1,z[N];
node w[N];
LL ans;
pair<LL,int>F[4];
void op(node &A,node &B,int w){
F[0]={A.p1,A.c1};
F[1]={A.p2,A.c2};
F[2]={B.p1+w,B.c1};
F[3]={B.p2+w,B.c2};
sort(F,F+4);
A.c1=A.c2=0;
for(int i=0;i<4;i++)
if(!A.c1)A.c1=F[i].se,A.p1=F[i].fi;
else if(A.c1!=F[i].se){A.c2=F[i].se,A.p2=F[i].fi;break;}
}
int gf(int x){
return x==t[x]?x:(t[x]=gf(t[x]));
}
void dfs1(int u,int fa){
f[u]={a[u],(LL)1e18,gf(u),0};
for(auto E:e[u]){
int v=E.fi;
if(v!=fa)dfs1(v,u);
else W[u]=E.se;
}
if(frst){
if(fa)e[u].erase(lower_bound(e[u].begin(),e[u].end(),make_pair(fa,0)));
l[u].resize(e[u].size());
r[u].resize(e[u].size());
}
if(e[u].empty())return;
l[u][0]={(LL)1e18,(LL)1e18,0,0};op(l[u][0],f[e[u][0].fi],e[u][0].se);
r[u][e[u].size()-1]={(LL)1e18,(LL)1e18,0,0};op(r[u][e[u].size()-1],f[e[u].back().fi],e[u].back().se);
for(int i=1;i<e[u].size();i++)l[u][i]=l[u][i-1],op(l[u][i],f[e[u][i].fi],e[u][i].se);
for(int i=e[u].size()-2;i>=0;i--)r[u][i]=r[u][i+1],op(r[u][i],f[e[u][i].fi],e[u][i].se);
op(f[u],r[u][0],0);
}
void dfs2(int u){
// printf("%d\n",u);
op(f[u],g[u],W[u]);
for(int i=0;i<e[u].size();i++){
int v=e[u][i].fi,w=e[u][i].se;
g[v]={a[u],(LL)1e18,gf(u),0};
op(g[v],g[u],W[u]);
if(i)op(g[v],l[u][i-1],0);
if(i<e[u].size()-1)op(g[v],r[u][i+1],0);
dfs2(v);
}
}
signed main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),t[i]=i;
for(int i=1,x,y,z;i<n;i++)
scanf("%d%d%d",&x,&y,&z),e[x].push_back({y,z}),e[y].push_back({x,z});
for(int i=1;i<=n;i++)sort(e[i].begin(),e[i].end());
m=n;
while(m>1){
for(int i=1;i<=n;i++)w[i]=g[i]={(LL)1e18,(LL)1e18,0,0};
dfs1(1,0);frst=0;
dfs2(1);
for(int i=1;i<=n;i++)op(w[gf(i)],f[i],a[i]),z[i]=i==gf(i);
for(int i=1;i<=n;i++)if(z[i]){
int y;LL x;
w[i].c1=gf(w[i].c1);w[i].c2=gf(w[i].c2);
if(w[i].c1!=gf(i))x=w[i].p1,y=w[i].c1;
else x=w[i].p2,y=w[i].c2;
if(y!=gf(i))t[y]=i,ans+=x,m--;
}
}
printf("%lld\n",ans);
return 0;
}
- ABC411
- E:求全局最大值,所以全部放一起排序。然后对 \(x\) 求 \(\max \le x\) 的方案数是简单的(每个骰子在前缀中的值个数乘积,求出 \(1\sim 6\) 逆元简单维护),差分带权求和。
- G:模板环计数。二元环分开做。多元环状压 dp 求 \(s\) 到 \(t\) 的路径数再加入 \((s,t)\) 这条边。这样 \(2^n n^3\),不行。这题的东西仍然经典:钦定环在最小点计算。这样复杂度变成 \(\sum 2^ii^2< 2^{n+1}n^2\) 就赢了,相当跑不满。
- ABC410
-
F:牛魔再做不出这种题我去世了,脑残吧。设 \(m<n\),直接得到 \(m\le \sqrt{nm}\), 那么 \(nm^2\) 就是合理的,这样能获得一行两个点,把二维前缀和的式子移项,前面维护、后面枚举。直接统计。脑残。脑残。
就这样一个小技巧,像是一类肿瘤一样,长在各路出题人脑子里。梦熊出一次,CF 出一次,AT 还有一次。
-
G:这题真好。先在此假设 \(a_i<b_i\)。环很麻烦,考虑在 \(1\) 处破开,那么答案形如

按 \(b\) 排序,求 \(f_i\) 表示前 \(i\) 个弦的最长嵌套(\(a_i<a_j<b_j<b_i\))链,可以用树状数组维护。枚举最后一个红色弦 \(r\),求出最后一个可能的蓝色弦 \(l\) 使 \(b_l<a_r\),答案是 \(f_l+f_r\),取最大值。
为什么中间跳过了几场?因为没价值或偏难。
ABC409G:很复杂的概率式子推导,但主要还是由于需要用到 NTT 而我还不会。
ABC408G:用到 SBT。LCA 讲了但我仍然不会,留着以后来。
- ABC407
- F:非常平庸。用单调栈可以直接做,只需要枚举短的那边然后用差分实现区间加,笛卡尔树能够分析出这是一个 \(\log\)。不过差分后的单点加居然也是区间加,可以二阶差分做到线性。
- G:用到网络流(费用流),我需要重新学习。
- E:这题挺不错。似乎早年出现在模拟赛中过,这一次自己做出来也并非什么成就。不可能 dp,这很明显。从左到右贪心,先假设选右括号,只要非法就将左侧权值最大的右括号改为左括号。最后一定获得合法括号串,最优证明是该策略和官方题解实则一致。
- ABC405
-
G:莫队。扔。
-
E:已经存在有三道我没做出来的、需要精细考虑的题目:AT_abc279_g At Most 2 Colors、P14636 NOIP2025 sale、这题。比较有特征的是,\(1\) 和 \(3\) 的位置关系与 \(2\) 和 \(4\) 的位置关系是相似且偏强力的。

可以发现,只需要确定 \(1,3\) 位置,\(2,4\) 就只能从左往右依次放置。在保证 \(4\) 在 \(1\) 右侧的前提下,枚举 \(1\) 的末尾 \(i\le a+b\),再将 \(1,3\) 放置。预处理组合数容易模拟。
-
F:同样很经典。两个弦相交当且仅当这两个区间相交且不包含。离线二维数点。
1.8
可以见得,现存有很多难题因为知识点不足而难以投入思考,但是就我在 NOIP2025 BC 中的表现来看,我实际上于可思考题也并没有达到完备的水准。
对于知识点,我只有一步一步学。但题目?我希望找一些不要充斥着未知难度板子的难题。这样对我达到有限完备水准更有帮助。
关于这一点可以和 LCA 进一步交流。
仍然非常经典。
考虑一个答案 \(\{a_m\}\),如果存在 \(s_{a_{i+1}}+s_{a_{i}}<s_{a_i}+s_{a_{i+1}}\),可以交换这两项得到更优解(注意这里如果考虑 \((i,j)\) 而非 \((i,i+1)\) 无法得到结论)。
那么我们就尝试重载 cmp 为 \([x+y<y+x]\),如果这是一个严格弱序关系,我们就可以做到 dp。
可以证明 cmp 等价于 \([x^\infty<y^\infty]\),确实是严格弱序。具体证明不难,不过另有结论:若 \(x+y=y+x\),则 \(x,y\) 均有长度为 \(\gcd(|x|,|y|)\) 的相等循环节。
排序后 dp。考虑到字典序比较从头开始,可以倒序 dp,这样构造解的时候容易比较。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=60;
int n,m;
string f[N][N],a[N];
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+n+1,[](string x,string y){return x+y<y+x;});
for(int i=n;i>=1;i--){
for(int j=1;j<=min(n-i+1,m);j++)
f[i][j]=min(a[i]+f[i+1][j-1],(j<n-i+1?f[i+1][j]:"~"));
}
cout<<f[1][m]<<'\n';
return 0;
}
一个很直接的想法是奇偶位置分别重排,然后再求逆序对数。
然后就错了。原因是 1 操作中途可能可以用 2 操作节省两次。
从单点贡献上重新考虑,一个数现在位于 \(i\),最终位于 \(j\),那么若 \(2|(i-j)\),不需要 1 操作。否则需要恰好一次来改变奇偶性。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e5+10;
int n,a[N],w[N];
signed main(){
// freopen("01.txt","r",stdin);
// freopen("a.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
w[i]=a[i];
}
sort(w+1,w+n+1);
for(int i=1;i<=n;i++)a[i]=lower_bound(w+1,w+n+1,a[i])-w;
int ans=0;
for(int i=1;i<=n;i++)ans+=abs(i-a[i])%2;
printf("%d\n",ans/2);
return 0;
}
先剔除一些无用信息:如果存在 \(a_i\ge a_j\),可以见得 \(a_i\) 无用。所以在提前将 \(n\) 设为 \(a_0\) 后仅保留严格后缀最小值,放入 \(\{b_m\}\) 数组,翻转后 \(b\) 递减。
通过模拟发现,题面中前缀截取多次后可能会使序列非常复杂。不能用常规手段维护。
于是尝试单步归纳。考虑最终态 \(b_1\),它由 \(b_{2}\) 重复 \(\lfloor\frac{b_{1}}{b_{2}}\rfloor\) 次、后接 \(b_2\) 的 \([1,b_1\bmod b_2]\) 这一前缀组成。
令 \(x=b_1\bmod b_2\),则 \(b_2\) 中的 \([1,x]\) 由 \(b_3\) 重复 \(\lfloor\frac{x}{b_3}\rfloor\) 次,后接 \(b_3\) 的 \([1,x\bmod b_3]\) 这一前缀组成。
……
类似地推广,如果 \(b_i\) 被 \(b_{[1,i-1]}\) 重复了 \(c_i\) 次,按上述方式计算 \(i\) 的重复对 \(b_j(j>i)\) 重复次数的贡献。
依次对 \(b_j(j>i)\) 取模完毕的 \(x\),表示后接 \(\{1,2,3,\dots,b_m\}\) 这一序列的 \([1,x]\) 前缀,运用差分将 \(s_{[1,x]}\) 加上 \(c_i\) 即可。
关于此流程的模拟,可以用二分找到第一个 \(b_k\le x\) 的 \(k\),令 \(x\overset\bmod\gets b_k\) 并计算贡献。可以分析复杂度为 \(\Theta(n\log V\log n)\)。
\(s\) 数组即答案。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e5+10;
int n,m;
LL len,a[N],mn[N],b[N],c[N],s[N];
signed main(){
scanf("%lld%d",&a[1],&n);
for(int i=2;i<=n+1;i++)scanf("%lld",&a[i]);
n++;
mn[n+1]=2e18;
for(int i=n;i>=1;i--){
mn[i]=min(mn[i+1],a[i]);
if(a[i]<mn[i+1])b[++m]=a[i];
}
int L,R,mid,j;
c[1]=1;
for(int i=1;i<=m;i++){
LL x=b[i],j=i;
while(j<m){
L=j+1;R=m;j=m;
while(L<=R){
mid=L+R>>1;
if(b[mid]<=x)j=mid,R=mid-1;
else L=mid+1;
}
c[j]+=c[i]*(x/b[j]);
x%=b[j];
}
s[1]+=c[i];s[x+1]-=c[i];
}
for(int i=1;i<=a[1];i++)s[i]+=s[i-1],printf("%lld\n",s[i]);
return 0;
}
这道题存在有非常好的子问题分治结构。
这是一个环,那么先想办法断开。可以见得最小值一定是自己直接买,以其为起点。
枚举轮换次数 \(i\),那么对于 \(j\),不可能从 \([1,j-i)\) 的位置轮换过来。而 \([j-i,j]\) 中的任意位置 \(k\),都可以选择在第 \(j-k\) 次轮换前买入,然后恰好轮换到 \(j\)。
所以 \(j\) 处付出的代价是 \(\min\limits_{k=j-i}^j a_k\),直接模拟。用笛卡尔树来差分算贡献应该能做到线性。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=4e3+10;
int n,a[N],m,q[N],l,r;
LL ans=1e18;
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
int st=1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]),st=a[i]<a[st]?i:st,a[i+n]=a[i];
if(n==1)return printf("%d\n",a[1]),0;
for(int i=0;i<n;i++){
LL res=0;l=1;r=0;
for(int j=st;j<st+n;j++){
while(l<=r&&a[q[r]]>=a[j])r--;q[++r]=j;
while(l<=r&&q[l]<j-i)l++;
res+=a[q[l]];
}
ans=min(ans,1ll*i*m+res);
}
printf("%lld\n",ans);
return 0;
}
此前选择用 dp 来算答案,导致优化不了,又盲猜一手三分,还写一遍退火,直接坠。
令人惊讶的是,这居然是铜牌题。
首先转化成移动出口,在出口矩形外的就死了。然后由于限制都表示为矩形,合理试证最后的路径也是走完一个矩形。易证。
状态就有了。\(f_{u,d,l,r}\) 就是表示走完了一个矩形的答案(注意不一定救了其中的所有机器人)。接下来是拓展一行或一列。比如拓展上端,那么可以救下第 \(u-1\) 行的 \([l,r]\) 中未被摧毁的机器人。目前未被摧毁的位置集是一个容易计算的矩形,求交算答案。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e2+10;
int n,m,U,D,L,R,sx,sy,r;
short f[N][N][N][N],cr[N][N],cc[N][N],gu,gd,gl,gr,ans;
char a[N][N];
void Max(short &x,short y){
x=max(x,y);
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",a[i]+1);
for(int j=1;j<=m;j++)
if(a[i][j]=='E')f[i][i][j][j]=0,sx=i,sy=j;
else{
f[i][i][j][j]=-3e4;
if(a[i][j]=='o')cr[i][j]=cc[i][j]=1;
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cr[i][j]+=cr[i][j-1];
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
cc[j][i]+=cc[j-1][i];
for(int s=0;s<=n-1+m-1;s++){
for(int u=1;u<=sx;u++)
for(int d=sx;d<=n;d++)
for(int l=1;l<=sy;l++){
r=s-d+u+l;
if(r<sy||r>m)continue;
U=1+d-sx;D=n-(sx-u);L=1+r-sy;R=m-(sy-l);
// printf("%d %d %d %d %d %d %d %d %hd\n",U,D,L,R,u,d,l,r,f[u][d][l][r]);
if(u>1){
if(u>U)gu=cr[u-1][min(r,R)]-cr[u-1][max(l,L)-1];
else gu=0;
Max(f[u-1][d][l][r],f[u][d][l][r]+max((short)0,gu));
}
if(d<n){
if(d<D)gd=cr[d+1][min(r,R)]-cr[d+1][max(l,L)-1];
else gd=0;
Max(f[u][d+1][l][r],f[u][d][l][r]+max((short)0,gd));
}
if(l>1){
if(l>L)gl=cc[min(d,D)][l-1]-cc[max(u,U)-1][l-1];
else gl=0;
Max(f[u][d][l-1][r],f[u][d][l][r]+max((short)0,gl));
}
if(r<m){
if(r<R)gr=cc[min(d,D)][r+1]-cc[max(u,U)-1][r+1];
else gr=0;
Max(f[u][d][l][r+1],f[u][d][l][r]+max((short)0,gr));
}
Max(ans,f[u][d][l][r]);
}
}
printf("%hd\n",ans);
return 0;
}
首先,这是一个构造问题。其次,每个音只用一次。那肯定不是让你挑战 NPC,所以大概和欧拉路径有关。
我们试着建模。比较容易察觉的结构是,音量和音高应是严格交替改变的。考虑相邻两个音,它们有某一维是相等的;相似的是关于一个点,出入的两条边有一端是相同的。
那么将音量和音高放在左右部,离散化后将一个音的两维连边,跑欧拉路径就可以。这样就能够满足以上条件。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=4e5+10;
int T,n,a[N],b[N],d[N],w[N],s[N],t,h[N];
vector<pair<int,int>>e[N];
bool vis[N];
void dfs(int u){
for(int i=h[u];i<e[u].size();i=h[u]){
h[u]++;
int v=e[u][i].fi,w=e[u][i].se;
if(!vis[w])vis[w]=1,dfs(v),s[++t]=w;
}
}
void solve(){
scanf("%d",&n);t=0;
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i],&b[i]),b[i]+=1e9,w[i*2-1]=a[i],w[i*2]=b[i];
for(int i=1;i<=n+n;i++)
e[i].clear(),d[i]=0;
sort(w+1,w+n+n+1);
for(int i=1;i<=n;i++){
h[i]=h[i+n]=0;
a[i]=lower_bound(w+1,w+n+n+1,a[i])-w;
b[i]=lower_bound(w+1,w+n+n+1,b[i])-w;
e[a[i]].push_back({b[i],i});e[b[i]].push_back({a[i],i});
d[a[i]]++;d[b[i]]++;
vis[i]=0;
// printf("%d %d\n",a[i],b[i]);
}
int cnt=0,p=0;
for(int i=1;i<=n+n;i++)if(d[i]&1)cnt++,p=i;
if(cnt!=0&&cnt!=2)return puts("NO"),void();
if(cnt==0){for(int i=1;i<=n;i++)if(d[i])p=i;}
dfs(p);
if(t<n)return puts("NO"),void();
puts("YES");
for(int i=t;i>=1;i--)printf("%d ",s[i]);puts("");
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&T);
while(T--)solve();
return 0;
}
简单的暴力是 \(n^3\),即对于每一个根都按题意 dp。
然后就可以换根,换根的复杂度稍加分析可以知道,上界是所有路径长度和。复杂度 \(n^2L\)。
不过如果认真地推理,可以发现如果确定了所有城市位置:对于首都,合法位置集合的结构是相当简单的。你可以用以下流程推理:
- 以 \(1\) 为根。
- 如果有一个城市在另一对城市的路径上,首都无解。
- 如果不存在祖先后代关系,首都只要不放在城市子树里就好了。
- 否则存在,那么一定有一个 \(u\) 作为祖先城市,其余所有城市均在其某一个子树里,首都应放在 \(u\) 子树中、非其余城市子树中的任何一个位置。
- 如果求出 \(f_{i,j},g_{i,j}\) 分别表示在 \(i\) 子树中放置 \(j\) 个城市且不存在祖先后代关系的方案数、所有方案的城市子树大小和,以上答案都能够统计。
- 当只有一个城市,以上结论有可能不成立,手动计算。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=3e3+10,mod=1e9+7;
int n,f[N][N],g[N][N],sz[N],ans,t[N];
vector<int>e[N];
void add(int &x,int y){if((x+=y)>=mod)x-=mod;}
void dfs(int u,int fa){
sz[u]=1;
f[u][0]=1;
for(int v:e[u])if(v!=fa){
dfs(v,u);
for(int i=sz[u];i>=0;i--)
for(int j=sz[v];j>0;j--)
add(f[u][i+j],1ll*f[u][i]*f[v][j]%mod),
add(g[u][i+j],(1ll*f[u][i]*g[v][j]+1ll*f[v][j]*g[u][i])%mod);
sz[u]+=sz[v];
for(int i=1;i<=sz[v];i++)add(t[i+2],(1ll*f[v][i]*sz[v]%mod-g[v][i]+mod)%mod);
}
if(u==1)for(int i=2;i<n;i++)add(t[i+1],(1ll*f[1][i]*n%mod-g[1][i]+mod)%mod);
add(f[u][1],1);add(g[u][1],sz[u]);
}
signed main(){
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),e[x].push_back(y),e[y].push_back(x);
dfs(1,0);
t[1]=n;t[2]=1ll*n*(n-1)%mod;
for(int i=1;i<=n;i++)printf("%d ",t[i]);
return 0;
}
这道题是不是有一点点像 P10785 NOI2024 集合?
你必须考虑到的一点是,答案有单调性,所以先二分。
那么你现在想要判定,是否在两个串中存在有一对长度为 \(len\) 的子串,使得每一种字母的出现位置集合相同。
信息量十分大,可以使用哈希。首先你理应无视字母本身的内容,只关心位置。这里也存在有很多种哈希办法:
- 把一个位置上的值设为“到下一个相等字母位置的距离”,然后前缀哈希。当子串区间向右滑动一位时,有一段哈希值需要更新。
- 按字母分离,每一种字母独立做一次前缀哈希(比如:把
x存在的位置赋为 \(1\),其他位置赋为 \(2\)),再以某种方式复合起来。
只需在 \(s\) 中模拟一次如上方法之一,放入 map;再在 \(t\) 中同样模拟,于 map 中查询 \((i-len,i]\) 的哈希值即可判定。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e5+10;
int n,m,p[N],q[26];
char s[N],t[N];
unsigned LL h[N],b[N];
map<unsigned LL,bool>g;
bool chk(int len){
memset(q,0,sizeof(q));
g.clear();
for(int i=1;i<=n;i++){
p[i]=0;
if(q[s[i]-'a']){
p[q[s[i]-'a']]=i-q[s[i]-'a'];
for(int j=q[s[i]-'a'];j<i;j++)h[j]=h[j-1]*13331+p[j];
}
q[s[i]-'a']=i;
h[i]=h[i-1]*13331;
if(i>=len)g[h[i]-h[i-len]*b[len]]=1;
}
memset(q,0,sizeof(q));
for(int i=1;i<=m;i++){
p[i]=0;
if(q[t[i]-'a']){
p[q[t[i]-'a']]=i-q[t[i]-'a'];
for(int j=q[t[i]-'a'];j<i;j++)h[j]=h[j-1]*13331+p[j];
}
q[t[i]-'a']=i;
h[i]=h[i-1]*13331;
if(i>=len&&g.count(h[i]-h[i-len]*b[len]))return 1;
}
return 0;
}
signed main(){
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
scanf("%s%s",s+1,t+1);n=strlen(s+1);m=strlen(t+1);
b[0]=1;
for(int i=1;i<=max(n,m);i++)b[i]=13331*b[i-1];
int L=0,R=min(n,m),ans=0,mid;
while(L<=R){
mid=L+R>>1;
if(chk(mid))ans=mid,L=mid+1;
else R=mid-1;
}
printf("%d\n",ans);
return 0;
}
先试着处理 \(a_i,b_i\) 的相消部分。枚举未消部分的总和 \(k=\sum|a-b|\),令剩余 \(n-k\) 个球一红一蓝绑定,即将 \(\frac{(n-k)}2\) 个“球对”放入 \(m\) 个箱子里,允许为空。
然后,每个箱子蓝色多还是红色多并不重要。那么将 \(k\) 个额外的球放入箱子后,有 \(2^m\) 种方式钦定它们的颜色。
最后,想办法把 \(k\) 个球放进去。重新描述:有 \(m\) 个箱子,你要在箱子 \(i\) 里放入 \(x_i\ge 1\) 个球,求每一种满足 \(\sum x_i=k\) 方案的 \(\prod x_i\) 之和。
再次转换。先插 \(m-1\) 个板在 \(k\) 个球的空隙中,再在每对板间分别选出一个代表球。这相当于一共有 \(k+m-1\) 个位置,你要选出 \(2m-1\) 个位置钦定它们交替地作为代表球、插板、代表球……
如上,对于 \(k\ge m,2|(n-k)\),方案数为 \(2^m\binom{\frac{n-k}2+m-1}{m-1}\binom{k+m-1}{2m-1}\),处理组合数。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=2e7+10,mod=998244353;
int n,m,fct[N],inv[N],pw[N],ans;
int qpow(int x,int y){
int z=1;
while(y){
if(y&1)z=1ll*z*x%mod;
y>>=1;x=1ll*x*x%mod;
}
return z;
}
int C(int x,int y){
if(x<y)return 0;
return 1ll*fct[x]*inv[x-y]%mod*inv[y]%mod;
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
fct[0]=inv[0]=pw[0]=1;
for(int i=1;i<=n+n;i++)fct[i]=1ll*i*fct[i-1]%mod,pw[i]=2*pw[i-1]%mod;
inv[n+n]=qpow(fct[n+n],mod-2);
for(int i=n+n-1;i>=1;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
for(int i=m;i<=n;i++)if((n-i)%2==0)
ans=(ans+1ll*C((n-i)/2+m-1,m-1)*pw[m]%mod*C(i+m-1,m+m-1))%mod;
printf("%d\n",ans);
return 0;
}
这题的关键一招是:把和贡献无关的部分拆出,用数学方法计算。
可以发现的是,这篇文章里写的前三个题目,都存在有非常强的顺序限制,并且能够简单地合并信息,无论是题目要求还是调整得到。
我们先考虑这题的顺序限制。
- 题目给出了树形结构限制。
- 优先选择打 \(a<b\) 的怪物,其中优先选择 \(a\) 小的点。(调整证明)
- 其次选择打 \(a\ge b\) 的怪物,其中优先选择 \(b\) 大的点。(调整证明)
据此能够比较两个怪物的信息。
再考虑合并方法。
- 希望将依次进行 \(x\gets x-a_1+b_1\),\(x\gets x-a_2+b_2\) 合并为一次操作 \(x\gets x-a_3+b_3\)。
- 考察 \(a_3\)。可知打这两个怪物有限制 \(x\ge \max(a_1,a_2+a_1-b_1)\),此限制现由 \(a_3\) 给出。故 \(a_3=a_1+\max(0,a_2-b_1)\)。
- 分讨 \(a_2-b_1\) 的正负性,加之 \(-a_1-a_2+b_1+b_2=-a_3+b_3\),得到 \(b_3=b_2+\max(0,b_1-a_2)\)。
据此能进行贪心合并。
准许入手。按以上方式对怪物排序,顺序判定。判定怪物 \(i\):
- 如果 \(fa_i\) 未被打掉,那么在打掉 \(fa_i\) 后必然立刻打 \(i\)(调整证明),合并两者。
- 否则直接打 \(i\),等同于将其与点 \(1\) 合并。
合并后,可以利用 set 重新维护顺序。
答案即最终的 \(a_1\)。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e5+10;
int n,T,p[N],f[N];
LL a[N],b[N];
struct node{
LL x,y;int z;
const bool operator<(const node &val)const{
if((val.x<val.y)!=(x<y))return (val.x<val.y)<(x<y);
else if(x<y)return x==val.x?(y==val.y?z<val.z:y>val.y):x<val.x;
else return y==val.y?(x==val.x?z<val.z:x<val.x):y>val.y;
}
};
set<node>s;
vector<int>e[N];
int gf(int x){return x==p[x]?x:(p[x]=gf(p[x]));}
void dfs(int u,int fa){
f[u]=fa;
for(int v:e[u])if(v!=fa)dfs(v,u);
}
void solve(){
scanf("%d",&n);
e[1].clear();p[1]=1;a[1]=b[1]=0;
for(int i=2;i<=n;i++)scanf("%lld%lld",&a[i],&b[i]),s.insert({a[i],b[i],i}),p[i]=i,e[i].clear();
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),e[x].push_back(y),e[y].push_back(x);
dfs(1,0);
while(s.size()){
int i=s.begin()->z,j=gf(f[i]);s.erase(s.begin());
if(j>1)s.erase({a[j],b[j],j});
a[j]+=max(0ll,a[i]-b[j]);
b[j]=b[i]+max(0ll,b[j]-a[i]);
p[i]=j;
if(j>1)s.insert({a[j],b[j],j});
}
printf("%lld\n",a[1]);
}
signed main(){
scanf("%d",&T);
while(T--)solve();
return 0;
}
如果改动的边不在原最短路上,求经过 \(e_j\) 的最短路和原最短路的最小值。
否则,答案是经过 \(e_j\) 的最短路与不经过 \(e_j\) 的最短路的最小值。
不经过 \(e_j\) 的最短路,即删边最短路。考虑新最短路形态,和原最短路应有一公共前缀、一公共后缀,枚举新极短路上、不在原最短路的一条边 \((u,v)\)。
设 \(l_u\) 表示所有 \(1\to u\) 最短路与原最短路的最短公共前缀位置。类似地 \(r_v\) 表示所有 \(v\to n\) 最短路与原最短路的最短公共后缀位置。原最短路上的第 \(i\) (0-index)个点 \(g_i\) 初始有 \(l_{g_i}=i,r_{g_i}=i+1\)。在 \(st=1,n\) 的两个最短路 DAG 上分别转移,分别是 \(r_y=\max(r_x,r_y)\),\(l_y=\min(l_y,l_x)\)。注意起点状态已初始化,不允许在原最短路上转移。
若 \(a_i\) 为删去边 \(e_i\) 后全图的最短路,应操作:\(a_{[l_u+1,r_v-1]}\) 对 \(d_{1,u}+w+d_{v,n}\) checkmin(离线可以用 multiset 扫描维护),即删除这段区间中的边均不影响此极短路。关于正确性:唯一的阻碍在于边两端取局部最短路是否会忽视一些路径。答案是不会。画图证明容易。
然后回到开头,简单输出答案。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=2e5+10;
int n,m,q,st[2],l[N],r[N],h[N],p[N],c[N];
struct edge{int x,y,z;}f[N];
vector<edge>e[N];
vector<pair<LL,bool>>t[N];
vector<int>g;
LL d[2][N],a[N];
multiset<LL>s;
void dijk(int o){
priority_queue<pair<LL,int>>q;
for(int i=1;i<=n;i++)d[o][i]=1e18,h[i]=0;
q.push({d[o][st[o]]=0,st[o]});
while(q.size()){
int u=q.top().se;LL D=-q.top().fi;
q.pop();
if(D!=d[o][u])continue;
for(auto E:e[u]){
int v=E.x,w=E.y;
if(d[o][v]>D+w)q.push({-(d[o][v]=D+w),v}),h[v]=E.z;
}
}
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d%d",&n,&m,&q);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
e[x].push_back({y,z,i}),e[y].push_back({x,z,i});
f[i]={x,y,z};
c[i]=-1;
}
st[0]=1;st[1]=n;dijk(0);dijk(1);
int k=1;
while(k){
g.push_back(k);
c[h[k]]=g.size();
k=f[h[k]].x==k?f[h[k]].y:f[h[k]].x;
}
for(int i=1;i<=n;i++)l[i]=n;
for(int i=0;i<g.size();i++)l[g[i]]=i;
for(int i=1;i<=n;i++)p[i]=i;
sort(p+1,p+n+1,[](int x,int y){return d[0][x]<d[0][y];});
for(int u=1;u<=n;u++)for(auto E:e[p[u]]){
int v=E.x,w=E.y;
if(c[E.z]==-1&&d[0][v]==d[0][p[u]]+w)l[v]=min(l[v],l[p[u]]);
}
for(int i=1;i<=n;i++)r[i]=0;
for(int i=0;i<g.size();i++)r[g[i]]=i+1;
sort(p+1,p+n+1,[](int x,int y){return d[1][x]<d[1][y];});
for(int u=1;u<=n;u++)for(auto E:e[p[u]]){
int v=E.x,w=E.y;
if(c[E.z]==-1&&d[1][v]==d[1][p[u]]+w)r[v]=max(r[v],r[p[u]]);
}
// for(int i=1;i<=n;i++)printf("%d %d\n",l[i],r[i]);
for(int i=1;i<=m;i++){
int x=f[i].x,y=f[i].y,z=f[i].z;
if(c[i]!=-1||d[0][x]==1e18||d[1][y]==1e18)continue;
LL D=d[0][x]+z+d[1][y];
if(l[x]<r[y]-1)t[l[x]+1].push_back({D,1}),t[r[y]].push_back({D,0});
D=d[1][x]+z+d[0][y];
if(l[y]<r[x]-1)t[l[y]+1].push_back({D,1}),t[r[x]].push_back({D,0});
}
for(int i=1;i<g.size();i++){
for(auto j:t[i])
if(j.se)s.insert(j.fi);
else s.erase(j.fi);
if(s.size())a[i]=*s.begin();
else a[i]=1e18;
}
while(q--){
int j,w;scanf("%d%d",&j,&w);
if(c[j]==-1)
printf("%lld\n",min(d[0][n],
min(d[0][f[j].x]+w+d[1][f[j].y],d[0][f[j].y]+w+d[1][f[j].x])));
else
printf("%lld\n",min(a[c[j]],
min(d[0][f[j].x]+w+d[1][f[j].y],d[0][f[j].y]+w+d[1][f[j].x])));
}
return 0;
}
这个问题没有要求判断无解,所以一定有解。我们可以随意强化成容易构造的样子,只要能反推原问题解就没有关系。
首先一个区间 \([l,r]\) 的所有性质都在 \(l-1,l,r,r+1\) 上,离散化。
考虑一个点如果被覆盖偶数次,则红蓝差为 \(0\);否则为 \(\pm1\)。尝试统一为最具特征的 \(0\),可以在所有被覆盖奇数次的极长区间上加一个区间来实现。
红色区间加,蓝色区间减,要求全局为 \(0\)。依旧差分,如果连无向边 \((l_i,r_i+1)\),正向走表示红,反向走表示蓝,那么解应表示为若干条欧拉回路。容易证明所有点度数在此图中为偶数,欧拉回路存在,按上述染色。
去除额外区间立刻得到原问题解。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e6+10;
int n,l[N],r[N],a[N],w[N],m,h[N],s[N],t,nn,c[N];
bool vis[N];
vector<pair<int,int>>e[N];
void dfs(int u){
for(int i=0;i<e[u].size();i=h[u]){
h[u]=i+1;
int v=e[u][i].fi,w=e[u][i].se;
if(!vis[w])vis[w]=vis[w^1]=1,dfs(v),s[++t]=w;
}
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&n);nn=n;
for(int i=1;i<=n;i++)
scanf("%d%d",&l[i],&r[i]),w[++m]=l[i],w[++m]=r[i],w[++m]=l[i]-1,w[++m]=r[i]+1;
sort(w+1,w+m+1);m=unique(w+1,w+m+1)-w-1;
for(int i=1;i<=n;i++)
l[i]=lower_bound(w+1,w+m+1,l[i])-w,r[i]=lower_bound(w+1,w+m+1,r[i])-w,a[l[i]]++,a[r[i]+1]--;
int L=0,R=0;
for(int i=1;i<=m+1;i++){
a[i]+=a[i-1];
// printf("%d ",a[i]);
if(a[i]&1){if(L)R=i;else L=R=i;}
else if(R){n++;l[n]=L;r[n]=R;L=R=0;}
}
for(int i=1;i<=n;i++)e[l[i]].push_back({r[i]+1,i*2}),e[r[i]+1].push_back({l[i],i*2+1});
for(int i=1;i<=m+1;i++)dfs(i),a[i]=0;
for(int i=1;i<=t;i++)c[s[i]/2]=s[i]%2;
for(int i=1;i<=nn;i++){
if(c[i])a[l[i]]++,a[r[i]+1]--;
else a[l[i]]--,a[r[i]+1]++;
// printf("%d %d\n",l[i],r[i]);
}
for(int i=1;i<=m+1;i++){
// printf("%d\n",a[i]+=a[i-1]);
assert(abs(a[i]+=a[i-1])<=1);
}
// puts("");
for(int i=1;i<=nn;i++)printf("%d ",c[i]);
return 0;
}
\(\overset{\mathbf{Maximum\ Prefix}}{\text{妈克西马木普利非克斯}}\)这道题,你必须知道的是:状态中一定要有前缀和最大值,而顺序转移必须额外记录目前前缀和,因此一般倒推。设 \(f_{i,j}\) 表示后 \(i+1\) 位已确定、后 \(i+1\) 位的前缀和最大值为 \(j\) 的概率。转移 \(f_{i-1,j+1}\overset +\gets p_if_{i,j},f_{i-1,\max(0,j-1)}\overset +\gets (1-p_i)f_{i,j}\)。如果想求长度为 \(k\) 时的答案,你应该以 \(f_{k,0}=1\) 为起点来 dp。最后得到 \(ans_k=\sum f_{0,i}h_i\)。必须做 \(n\) 次。
这个算法为什么很残疾呢?dp 是允许求出单起点、多终点或单终点、多起点答案的。但以上算法钦定起点 \(f_{k,0}\) 又钦定终点 \(f_0\),蠢。
聪明一点的话,设 \(g_{i,j}\) 表示以 \(f_{i,j}\) 为起点再确定前 \(i\) 位的期望得分。终点仍然在 \(g_0\),有 \(g_{0,i}=h_i\)。转移 \(g_{i,j}=p_ig_{i-1,j+1}+(1-p_i)g_{i-1,\max(0,j-1)}\)。这样做就是单终点多起点的,\(k\) 的答案即 \(g_{k,0}\)。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=5e3+10,mod=1e9+7;
int n,p[N],f[N][N],T;
int qpow(int x,int y){
int z=1;
while(y){
if(y&1)z=1llu*z*x%mod;
y>>=1;x=1llu*x*x%mod;
}
return z;
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x,y;scanf("%d%d",&x,&y);
p[i]=1llu*x*qpow(y,mod-2)%mod;
}
for(int i=0;i<=n;i++)scanf("%d",&f[0][i]);
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++)
f[i][j]=(1ll*p[i]*f[i-1][j+1]+1ll*(mod+1-p[i])*f[i-1][max(0,j-1)])%mod;
printf("%d ",f[i][0]);
}
puts("");
}
return 0;
}
为方便取模,我们认为行、列、值均为 0-index。考虑一个位置的属性:位于 \((i,j)\),值为 \(a_{i,j}\)。
以 U 为例,属性变为位于 \((i-1\bmod n,j)\),值为 \(a_{i,j}\)。以 I 为例,属性变为位于 \((i,a_{i,j})\),值为 \(j\)。
这些变换如果想要高效地维护,必须允许合并,可以使用矩阵乘法。如果一个位置表示为向量 \([i,j,a_{i,j},1]\),容易对 6 种操作分别构造矩阵。
最后用初始向量乘上操作矩阵就可以迅速求出该点最后的属性。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;
const int N=1e3+10,M=1e5+10;
int n,m,a[N][N],b[N][N];
char op[M];
struct matrx{
int a[4][4],x,y;
const matrx operator*(const matrx &val)const{
matrx ret;ret.x=x;ret.y=val.y;
for(int i=0;i<ret.x;i++)
for(int j=0;j<ret.y;j++){
ret.a[i][j]=0;
for(int k=0;k<y;k++)(ret.a[i][j]+=a[i][k]*val.a[k][j])%=n;
}
return ret;
}
}G,R,L,D,U,I,C,F,H;
matrx E(){
matrx ret;ret.x=ret.y=4;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)ret.a[i][j]=(i==j);
return ret;
}
void solve(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)scanf("%d",&a[i-1][j-1]),a[i-1][j-1]--;
U.a[3][0]=n-1;
L.a[3][1]=n-1;
G=E();
scanf("%s",op+1);
for(int i=1;i<=m;i++){
if(op[i]=='R')G=G*R;
if(op[i]=='L')G=G*L;
if(op[i]=='D')G=G*D;
if(op[i]=='U')G=G*U;
if(op[i]=='I')G=G*I;
if(op[i]=='C')G=G*C;
}
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
F.a[0][0]=i;
F.a[0][1]=j;
F.a[0][2]=a[i][j];
H=F*G;
b[H.a[0][0]][H.a[0][1]]=H.a[0][2];
}
for(int i=0;i<n;i++,puts(""))
for(int j=0;j<n;j++)printf("%d ",b[i][j]+1);
puts("");
}
signed main(){
G=R=L=D=U=I=C=E();
D.a[3][0]=1;U=D;
R.a[3][1]=1;L=R;
I.a[2][1]=I.a[1][2]=1;I.a[1][1]=I.a[2][2]=0;
C.a[2][0]=C.a[0][2]=1;C.a[0][0]=C.a[2][2]=0;
F.x=1;F.y=4;F.a[0][3]=1;
int T;scanf("%d",&T);
while(T--)solve();
return 0;
}
这道题还可以加强为取出一个操作区间执行后单点求值,用线段树/猫树维护区间矩阵积。

浙公网安备 33010602011771号