被死亡凝视前还有一分钟去忘记了一切
[UNR 7]反重:求熵
好怪的题。
我们考虑一个一个消掉变量,现在考虑 \(x_n\),我们会有一堆形如:\(x_n\leq x_i+a_{n,i}\) 或者 \(x_n\geq x_i-a_{i,n}\) 的限制,显然第一类限制给出了 \(x_n\) 的上界,第二类限制给出了 \(x_n\) 的下界,如果已经确定了 \(x_1\cdots x_{n-1}\),只需要考虑两类限制中分别最紧的就行了。
我们枚举第一类限制中最紧的是 \(p\),第二类限制中最紧的是 \(q\) ,那么就是要求:
-
\(x_i+a_{n,i}\geq x_p+a_{n,p}\to x_p-x_i\leq a_{n,i}-a_{n,p}\)
-
\(x_i-a_{i,n}\leq x_q-a_{q,n}\to x_i-x_q\leq a_{i,n}-a_{q,n}\)
-
\(x_p+a_{n,p}\geq x_q-a_{q,n}\to x_q-x_p\leq a_{n,p}+a_{q,n}\)
这样就转化成了一堆和 \(n\) 无关的限制,递归去做,然后乘上 \(n\) 的取值区间大小即可。
我们注意到,每次相当于是乘了一个关于 \(x_p,x_q\) 的多项式,因此直接维护这 \(n\) 个多元多项式,更新的时候需要求自然数幂和,可以用伯努利数预处理。
这样大约是 \(O((n!)^2f(n))\),\(f(n)\) 是维护多项式的复杂度。
考虑优化,注意到我们的 \(p,q\) 其实没啥关系,换言之我们求出 \(p\) 的贡献,再求出 \(q\) 的贡献,再求和即可,这样复杂度变成 \(O(2^nn!f(n))\),可以过了。
注意更新限制的时候要去重,也就是加上一个 \([i<p]\) 之类的东西。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
const int mod = 998244353;
typedef long long LL;
int B[N],C[N][N],F[N][N];
int Pow(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
int inv[N];
void init(int n)
{
inv[1]=1;
for(int i=2;i<=n;i++)inv[i]=1ll*inv[mod%i]*(mod-mod/i)%mod;
C[0][0]=1;
for(int i=1;i<=n;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
B[0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
B[i]=(B[i]-1ll*C[i][j]*B[j]%mod*inv[i-j+1]%mod+mod)%mod;
for(int p=0;p<n;p++)
{
for(int j=0;j<=p;j++)
{
int v=1ll*inv[p+1]*C[p+1][j]%mod*B[j]%mod;
if(j&1)v=(mod-v)%mod;
F[p][p+1-j]=v;
}
}
}
LL a[N][N];
#define poly vector<int>
#define deg(x) (int)x.size()
#define up(x,v) x.resize(v)
void ckmin(LL &x,LL v){x=min(x,v);}
poly operator +(poly f,poly g)
{
if(deg(f)<deg(g))swap(f,g);
for(int i=0;i<deg(g);i++)f[i]=(f[i]+g[i])%mod;
return f;
}
poly operator *(poly f,int c)
{
for(int i=0;i<deg(f);i++)f[i]=1ll*f[i]*c%mod;
return f;
}
poly operator *(poly f,poly g)
{
int n=deg(f),m=deg(g);
poly h;up(h,n+m-1);
for(int i=0;i<n;i++)for(int j=0;j<m;j++)h[i+j]=(h[i+j]+1ll*f[i]*g[j]%mod)%mod;
return h;
}
struct node
{
poly f[N];
}cur;
int pw[N];
poly develop(poly f,LL d)
{
int n=deg(f);
d=(d%mod+mod)%mod;
poly g;up(g,n+1);
pw[0]=1;for(int i=1;i<=n;i++)pw[i]=1ll*pw[i-1]*d%mod;
for(int i=0;i<n;i++)
for(int j=0;j<=i+1;j++)
for(int k=0;k<=j;k++)
g[k]=(g[k]+1ll*C[j][k]*f[i]%mod*pw[j-k]%mod*F[i][j]%mod)%mod;
return g;
}
int ans;
void dfs(int n,int c,node P)
{
for(int i=0;i<=n;i++)
for(int j=i;j<=n;j++)
if(a[i][j]+a[j][i]<0)return;
if(!n)
{
ans=(ans+1ll*c*P.f[0][0]%mod)%mod;
return;
}
LL b[N][N];
for(int x=0;x<=n;x++)for(int y=0;y<=n;y++)b[x][y]=a[x][y];
for(int i=0;i<n;i++)
{
for(int x=0;x<=n;x++)for(int y=0;y<=n;y++)a[x][y]=b[x][y];
node Q=P;
for(int j=0;j<n;j++)
{
ckmin(a[j][i],a[j][n]-a[i][n]-(j<i));
ckmin(a[i][j],a[i][n]+a[n][j]);
}
Q.f[i]=Q.f[i]*develop(P.f[n],-a[i][n]-1);
dfs(n-1,mod-c,Q);
}
for(int i=0;i<n;i++)
{
for(int x=0;x<=n;x++)for(int y=0;y<=n;y++)a[x][y]=b[x][y];
node Q=P;
for(int j=0;j<n;j++)
{
ckmin(a[i][j],a[n][j]-a[n][i]-(j<i));
ckmin(a[j][i],a[j][n]+a[n][i]);
}
Q.f[i]=Q.f[i]*develop(P.f[n],a[n][i]);
dfs(n-1,c,Q);
}
}
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
int main()
{
init(12);
int n,m;LL T;
read(n);read(m);read(T);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=T;
for(int i=1;i<=n;i++)
{
a[0][i]=0;
a[i][0]=T;
}
for(int i=1;i<=m;i++)
{
int x,y;LL v;
read(x);read(y);read(v);
a[x][y]=min(a[x][y],v);
}
node P;
for(int i=0;i<=n;i++)P.f[i]={1};
dfs(n,1,P);
cout<<ans;
return 0;
}
[UR #26] 铁轨回收
我们可以考虑倒着来,维护一个 “待匹配的长度集合”,每次到当前位置的时候,枚举当前位置最终长度,然后选择一个集合内的元素减掉这个长度,再把当前位置需要匹配的长度扔进集合里。
但是因为大小要对 \(B\) 取 \(\min\),所以集合的状态数是爆炸的。
我们考虑容斥,对于一个位置,我们发现只有它长度 \(=B\) 时会出现这种情况,那我们考虑用所有情况减掉小于 \(B\) 的情况。
那么我们现在变成这样一个问题:
- 每个位置要么选择长度 \(<B\),要么选择求任意,要么选择 \(<B\) 的带上 \(-1\) 的容斥系数。同时,如果某个位置的父亲是任意,那么该位置也要求任意。
那么我们可以考虑一个 \(dp\),记录当前的任意个数,以及 “待匹配的长度集合” 中每种长度剩几个。
这样做的好处是:我们不管怎样选,长度集合的总长度是不增的,并且一开始的长度不超过 \(B\),所以总过程中长度集合的状态数是 B 的分拆数级别!
然后再上述 \(dp\) 即可,千万别写记忆化搜索,常数逆天。
code
#pragma GCC optimize(2)
#define GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 66;
int A[N],B[N],n;
const int mod = 998244353;
int Pow(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
inline int add(int a,int b){return a+b>=mod?a+b-mod:a+b;}
inline int sub(int a,int b){return a-b<0?a-b+mod:a-b;}
typedef unsigned long long ULL;
const ULL base = 1333331;
unordered_map<ULL,int> dp,id;
const int M = 30000;
int c[M][32],cnt[M],C[33],idx=0,ins[M][32],del[M][32],val[M];
int sum[N];
vector<int> pos[M];
void find_state(int x,int tot)
{
if(x==31)
{
ULL h=0;
++idx;
val[idx]=tot;
for(int j=1;j<=30;j++)
{
c[idx][j]=C[j];
cnt[idx]+=C[j];
h=h*base+C[j]+1;
if(C[j])pos[idx].push_back(j);
}
id[h]=idx;
return;
}
for(int j=0;j<=n&&tot+x*j<=B[n];j++)
{
C[x]=j;
find_state(x+1,tot+x*j);
}
}
int H=0;
int f[N][N][M];
int main()
{
read(n);
for(int i=1;i<=n;i++)read(A[i]),read(B[i]),sum[i]=sum[i-1]+A[i];
find_state(1,0);
for(int i=1;i<=idx;i++)
{
ins[i][0]=i;
del[i][0]=i;
for(int j=1;j<=30;j++)
{
ULL A=0,B=0;
for(int k=1;k<=30;k++)
{
if(k!=j)
{
A=A*base+c[i][k]+1;
B=B*base+c[i][k]+1;
}
else
{
A=A*base+c[i][k]+2;
B=B*base+c[i][k];
}
}
if(id.find(A)!=id.end())ins[i][j]=id[A];
if(id.find(B)!=id.end())del[i][j]=id[B];
}
}
int mul=1;
for(int i=1;i<n;i++)mul=1ll*mul*(n-i)%mod;
for(int j=0;j<A[n];j++)printf("%d ",0);
for(int k=0;k<=n;k++)
f[0][k][1]=1;
for(int i=1;i<n;i++)
for(int k=0;k<=n-i;k++)
for(int S=1;S<=idx;S++)
if(sum[i]>=val[S]&&n-i-k-cnt[S]>=0)
{
int res=0;
res=add(res,1ll*f[i-1][k+1][S]*k%mod);
if(B[i]==0)
{
int u=n-i-k-cnt[S];
if(u)res=add(res,1ll*u*f[i-1][k+1][S]%mod);
}
for(int j:pos[S])if(j>=B[i])
{
int u=c[S][j];
int T=S;
T=del[T][j];
T=ins[T][j-B[i]];
res=add(res,1ll*u*f[i-1][k+1][T]%mod);
for(int t=A[i];t<B[i];t++)
res=sub(res,1ll*u*f[i-1][k][ins[T][t-A[i]]]%mod);
}
if(A[i]==0&&B[i])
{
int u=n-i-k-cnt[S];
if(u)res=add(res,1ll*u*f[i-1][k][S]%mod);
}
for(int j:pos[S])if(j>=A[i])
{
int u=c[S][j];
for(int t=A[i];t<=min(j,B[i]-1);t++)
{
int T=S;
T=del[T][j];
T=ins[T][j-t];
T=ins[T][t-A[i]];
res=add(res,1ll*u*f[i-1][k][T]%mod);
}
}
f[i][k][S]=res;
}
int res=0;
for(int j=A[n];j<B[n];j++)
{
int v=f[n-1][0][ins[1][j-A[n]]];
printf("%lld ",1ll*v*Pow(mul,mod-2)%mod);
res=add(res,v);
}
printf("%lld ",1ll*sub(f[n-1][1][1],res)*Pow(mul,mod-2)%mod);
return 0;
}
[USACO24OPEN] Identity Theft P
考虑一个无限大的字典树,那么这 \(n\) 个串各代表了一个终止节点,我们每次可以花费一个代价把一个终止节点移动到某个儿子,使得最终任意两个终止节点不构成祖先关系。
考虑自底向上贪心,我们每次可以选择以下两种决策:
- 注意到每个时刻当前字典树的叶子恰好有一个终止节点,我们可以选择一个子树内的叶子,给这个叶子建两个儿子,并且把当前终止节点和叶子上的终止节点移动上去,代价是 \(d+2\),其中 \(d\) 为到叶子的距离。
- 选择一个子树内的,恰好有一个儿子的节点,给这点新建一个儿子,然后把当前终止节点移上去,代价是 \(d+1\)。
维护所有可能的决策,每次用最小代价的决策更新即可,决策的合并可以用可并堆,复杂度 \(O(N\log N)\),其中 \(N\) 为所有串的总长度和。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 5e6+7;
int tot=1;
char s[N];
int tr[N][2],cnt[N],d[N];
int ans=0;
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
int rot[N],lson[N],rson[N],idx=0,dis[N];
PII val[N];
int Merge(int x,int y)
{
if(!x||!y)return x+y;
if(val[x]>val[y])swap(x,y);
if(dis[lson[x]]<dis[rson[x]]) swap(lson[x],rson[x]);
dis[x]=dis[rson[x]]+1;
rson[x]=Merge(rson[x],y);
return x;
}
void dfs(int x)
{
if(!tr[x][0]&&!tr[x][1])
{
int u=++idx;
val[u]=mk(d[x]+2,x);
rot[x]=u;
}
else if(!tr[x][0]||!tr[x][1])
{
int u=++idx;
val[u]=mk(d[x]+1,x);
rot[x]=u;
}
for(int c=0;c<=1;c++)
if(tr[x][c])
{
d[tr[x][c]]=d[x]+1;
dfs(tr[x][c]);
rot[x]=Merge(rot[x],rot[tr[x][c]]);
}
if(!tr[x][0]&&!tr[x][1])cnt[x]--;
while(cnt[x])
{
cnt[x]--;
int u=rot[x],p=val[u].second;
ans+=val[u].first-d[x];
rot[x]=Merge(lson[u],rson[u]);
if(!tr[p][0]&&!tr[p][1])
{
tr[p][0]=++tot;d[tot]=d[p]+1;
int v=++idx;
val[v]=mk(d[tot]+2,tot);
rot[x]=Merge(rot[x],v);
tr[p][1]=++tot;d[tot]=d[p]+1;
v=++idx;
val[v]=mk(d[tot]+2,tot);
rot[x]=Merge(rot[x],v);
}
else
{
if(!tr[p][0])tr[p][0]=++tot,d[tot]=d[p]+1;
if(!tr[p][1])tr[p][1]=++tot,d[tot]=d[p]+1;
int v=++idx;
val[v]=mk(d[tot]+2,tot);
rot[x]=Merge(rot[x],v);
}
}
}
int main()
{
//freopen("a.in","r",stdin);
int n;
read(n);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
int p=1,m=strlen(s+1);
for(int j=1;j<=m;j++)
{
int c=s[j]-'0';
if(!tr[p][c])tr[p][c]=++tot;
p=tr[p][c];
}
cnt[p]++;
}
dfs(1);
cout<<ans;
return 0;
}
[USACO24OPEN] Splitting Haybales P
讲个笑话,这题过了才发现保证了 \(a_1\geq a_2\geq \cdots a_n\) 的性质。
记 \(x\) 为两人干草数量的差,每次相当于:
- 若 \(x\leq 0\),\(x=x+a_i\) 。
- 若 \(x>0\),\(x=x-a_i\) 。
我们可以先做一步预处理, 因为 \(x\) 在第一次转变正负前是操作固定的,我们可以先二分出第一个转变正负的位置,那么此时必然有 \(|x|\leq 2\times 10^5\),并且之后一直满足。
然后的做法和 P8264 基本相同,我们考虑分块,散块暴力操作,整块维护出每个初始值变成了啥。
我们初始令 \([l,r]=[1,2\times 10^5]\),表示当前还存在的值,那么当遇到一个 \(a_i\) 时,我们发现整体平移等价于平移原点,因此我们维护再维护当前的原点 \(p\) 。
平移过后,我们考察一下,若两个点关于原点对称,那么他们最终变成的值也只有正负号的区别,因此我们不妨把他们合并成一个,可以把点的个数少的一边向另一边对应的点连边,并把小的一部分删掉,最后只需要遍历一下这个森林就能知道每个点的答案了。
原点的答案需要特殊处理一下,复杂度约为 \(O(qB+\frac{n}{B}(V+q))\),一点不卡常。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
int sign(LL x){return x>0;}
int n,m;
const int N = 2e5+7;
int a[N];
LL s[N];
inline LL f(LL x,LL v){return x>0?x-v:x+v;}
int ql[N],qr[N],qx[N];
int bel[N],B;
int fa[N],ans[N],spe[N];
int calc(int x,int l,int r)
{
for(int i=l;i<=r;i++)x=f(x,a[i]);
return x;
}
int vis[N],tag=0,tag2=0;
void get(int x)
{
if(vis[x]==tag||spe[x]==tag2)return;
vis[x]=tag;
get(fa[x]);
if(spe[fa[x]]==tag2)
{
ans[x]=ans[fa[x]];
spe[x]=tag2;
}
else
{
ans[x]=-ans[fa[x]];
}
}
int main()
{
read(n);
for(int i=1;i<=n;i++)
{
read(a[i]);
s[i]=s[i-1]+a[i];
}
read(m);
for(int i=1;i<=m;i++)
{
int L,R;
LL x;
read(L);read(R);read(x);
int l=L,r=R,pos=l-1;
while(l<=r)
{
int mid=(l+r)>>1;
if(sign(x)==sign(f(x,s[mid]-s[L-1])))
{
pos=mid;
l=mid+1;
}
else r=mid-1;
}
x=f(x,s[pos]-s[L-1]);
ql[i]=pos+1;qr[i]=R;qx[i]=x;
}
B=sqrt(n);
for(int al=1;al<=n;al+=B)
{
++tag;
++tag2;
int ar=min(n,al+B-1);
int l=1,r=2e5,p=0;
ans[0]=calc(0,al,ar);vis[0]=tag;spe[0]=tag2;
for(int i=l;i<=r;i++)fa[i]=i;
for(int i=al;i<=ar;i++)
{
int x=a[i];
if(p<l)p+=x;
else if(p>r)p-=x;
if(p<l||p>r)continue;
ans[p]=calc(0,i+1,ar);
vis[p]=tag;spe[p]=tag2;
if(p-l<r-p)
{
for(int i=l;i<p;i++)fa[i]=2*p-i;
l=p+1;
}
else
{
for(int i=r;i>p;i--)fa[i]=2*p-i;
r=p-1;
}
}
for(int i=l;i<=r;i++)ans[i]=i-p,vis[i]=tag;
for(int i=1;i<=m;i++)
{
if(qr[i]<al||ql[i]>ar)continue;
if(ql[i]<=al&&ar<=qr[i])
{
if(qx[i]==0)qx[i]=ans[0];
else
{
int v=abs(qx[i]);
get(v);
if(spe[v]==tag2)qx[i]=ans[v];
else
{
if(qx[i]<0)qx[i]=-ans[v];
else qx[i]=ans[v];
}
}
continue;
}
for(int j=max(al,ql[i]);j<=min(ar,qr[i]);j++)qx[i]=f(qx[i],a[j]);
}
}
for(int i=1;i<=m;i++)printf("%d\n",qx[i]);
return 0;
}
[USACO24OPEN] Activating Robots P
首先有个 naive 的 \(O(R2^RN^2)\) 做法,设 \(dp_{S,i}\) 表示激活了 \(S\) 内的机器人,当前在第 \(i\) 个激活点的最小时间,转移枚举下一个激活点即可。
考虑转化一下,给所有东西加一个顺时针的,\(\frac{1}{K}\) 单位每秒的速度,那么现在,机器人就不会动了,激活点会以该速度移动,人也还可以移动,只是顺时针和逆时针移动速度不一样。
假设我们想要在某个点放一个机器人,因为此时是激活点在动,所以我们的最优策略显然是等到第一个转过来的激活点然后操作,这个可以二分得到。
设 \(dp_{S,i}\) 为激活了 \(S\) 内的机器人,上一次激活的是 \(i\),转移时枚举下一个激活的机器人,并二分算一下时间,复杂度 \(O(2^RR^2\log n)\) 。
注意到我们可以先不二分,我们对于每个状态,先算出不考虑等待时间的最小时刻,等到计算完成后,再对着这个最小时刻二分得到 \(dp_{S,i}\) 。
复杂度 \(O(2^RR(R+\log n))\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+7;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int M = (1<<20)+7;
LL dp[M][22];
LL L,K,D;
int n,m;
LL a[N];
void ckmin(LL &x,LL v){x=min(x,v);}
LL ceil(LL x,LL y)
{
return (x+y-1)/y;
}
LL dist(LL x,LL y)
{
LL d=(x-y+L)%L;
LL ans=ceil(K*d,K+1);
if(K>1)ans=min(ans,ceil(K*(L-d),K-1));
return ans;
}
LL solve(int i,LL t)
{
LL P=(D*(i+1)+ceil(t,K))%L;
int p=lower_bound(a+1,a+n+1,P)-a;
LL d=a[p]-P;
return K*(d+ceil(t,K));
}
LL dis[N];
int main()
{
//freopen("a.in","r",stdin);
//freopen("a.ans","w",stdout);
read(L);read(m);read(n);read(K);D=L/m;
for(int i=1;i<=n;i++)read(a[i]);
sort(a+1,a+n+1);a[++n]=a[1]+L;
for(int S=0;S<(1<<(m-1));S++)for(int i=0;i<m-1;i++)dp[S][i]=1e18;
for(int i=0;i<m-1;i++)dp[(1<<i)][i]=dist(0,D*(i+1));
for(int i=0;i<m;i++)dis[i]=dist(0,D*i);
for(int S=0;S<(1<<(m-1));S++)
for(int i=0;i<m-1;i++)if((S>>i)&1)
{
dp[S][i]=solve(i,dp[S][i]);
for(int j=0;j<m-1;j++)if(!((S>>j)&1))
ckmin(dp[S^(1<<j)][j],dp[S][i]+dis[(j-i+m)%m]);
}
LL mn=1e18;
for(int i=0;i<m-1;i++)mn=min(mn,dp[(1<<(m-1))-1][i]);
cout<<mn;
return 0;
}
[PA 2022] Chodzenie po linie
有点牛的题。
首先,我们可以发现,每个连通块是原序列的一个区间,具体的,设 \(mx_i\) 为前 \(i\) 个数的最大值,那么若 \(mx_i=i\) 则 \(i\) 是某个区间的右端点,并且这是充要条件,证明可以看看 CF1270H。
然后我们对每个连通块分别处理。
假设现在要计算 \(i\) 的答案。
首先每个位置左边和右边是对称的,翻转序列和值域后再做一遍即可,我们现在只讨论前面。
我们把最短路之和 \(\sum_j dis_j\) 变成 \(\sum_{d}\sum_j[dis_j\geq d]\) ,这样只需要对每个 \(d\) 统计多少节点的 \(dis_j\geq d\) 即可。
- \(d=1\) ,显然 \(i-1\) 个点都是符合的,因为图连通。
- \(d=2\),可以一步到的只有 \(p_j>p_i\) 的点,所以贡献为 \(p_j<p_i\) 的点数。
- \(d=3\),我们考虑一下两步到的路径形态,先考虑第一步走的点向左的情况,则一定走到 \(p_j>p_i\) 且 \(j\) 最小的 \(j\) 最优,因为所有两步到的点一定满足 \(p_k<p_i\),\(j\) 越小显然这样的 \(k\) 越多。类似的,若第一步向右走,一定是走到 \(p_j\) 最小的 \(j\)。记左右两个点分别是 \(l_i,r_i\),此时不能在两步之内到的点是满足 \(k<l_i,p_k<p_{r_i}\) 的所有点,否则一定可以在两步内到达。
- \(d>3\),推广上述情况,我们可以发现,我们总是可以用一个区间 \([L,R]\) 表示一个状态,初始时 \(L=R=i\),每一次令 \(L=l_{R},R=r_{L}\),就可以求出下一个 \(d\) 的状态,然后它的贡献就是 \(k<L,p_k<p_R\) 的所有点,这个比较容易证。
那么我们把所有区间间的转化建成一个棵树,可以证明这个树的点数是 \(O(n\sqrt n)\) 级别的,那么我们现在就是要查询某个点到根的路径上的权值和,每个权值是一个矩形数点,在线数点应该要写个 \(n^{\frac{1}{4}}\) 叉树,比较麻烦,我们考虑离线。
从左到右扫描,每个点的父亲一定比儿子先扫到,用分块平衡数点复杂度,可以做到 \(O(n\sqrt n)\) 。
建树的过程可以直接哈希存状态然后 BFS,也可以利用单调性,从右到左维护以每个位置为左端点的区间,每次更新区间的时候,右端点是单调的,因此可以直接判断是不是重复区间。
总复杂度 \(O(n\sqrt n)\) 。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 5e5+7;
typedef long long LL;
LL ans[N],dis[N];
int p[N],n=0;
int l[N],r[N];
int m,a[N],pos[N];
int idx=0;
const int M = 3e7+7;
int lp[M],rp[M],fa[M],B;
vector<int> used[N];
LL tag[N],sum[N],val[M];
void upd(int x)
{
for(int i=x/B+1;i<=n/B;i++)tag[i]++;
for(int i=x;i/B==x/B;i++)sum[i]++;
}
void calc()
{
B=sqrt(n);
int mnv=1e9,mnp=0;
for(int i=n;i>=1;i--)
{
pos[p[i]]=i;
if(p[i]<mnv)
{
mnv=p[i];
mnp=i;
}
r[i]=mnp;
}
mnv=1e9;
for(int i=n;i>=1;i--)
{
mnv=min(mnv,pos[i]);
l[pos[i]]=mnv;
}
idx=0;
for(int i=1;i<=n;i++)
{
used[i].clear();
int x=++idx;
lp[x]=rp[x]=i;
used[i].push_back(x);
}
for(int i=n;i>=1;i--)
{
for(int j=0;j<used[i].size();j++)
{
int u=used[i][j];
int L=l[rp[u]],R=r[i];
if(used[L].empty()||R!=rp[used[L].back()])
{
int x=++idx;
lp[x]=L;rp[x]=R;
used[L].push_back(x);
}
fa[u]=used[L].back();
}
}
for(int i=0;i<=n;i++)tag[i]=sum[i]=0;
for(int i=1;i<=n;i++)
{
dis[i]=i-1;
for(int j=used[i].size()-1;j>=0;j--)
{
int x=used[i][j],v=p[rp[x]];
val[x]=val[fa[x]]+sum[v]+tag[v/B];
if(rp[x]==i)dis[i]+=val[x];
}
upd(p[i]);
}
for(int i=1;i<=idx;i++)lp[i]=rp[i]=val[i]=fa[i]=0;
}
void solve(int l,int r)
{
n=0;for(int i=l;i<=r;i++)p[++n]=a[i]-l+1;
calc();
for(int i=l;i<=r;i++)ans[i]+=dis[i-l+1];
reverse(p+1,p+n+1);
for(int i=1;i<=n;i++)p[i]=n-p[i]+1;
calc();
for(int i=l;i<=r;i++)ans[i]+=dis[r-i+1];
}
int main()
{
//freopen("a.in","r",stdin);
//freopen("a.ans","w",stdout);
read(m);
for(int i=1;i<=m;i++)read(a[i]);
int mx=0,lst=1;
for(int i=1;i<=m;i++)
{
mx=max(mx,a[i]);
if(mx==i)
{
solve(lst,i);
lst=i+1;
}
}
for(int i=1;i<=m;i++)printf("%lld ",ans[i]);
return 0;
}
P7811 [JRKSJ R2] 你的名字。
看起来就不太能polylog,直接根分。
- \(k\leq B\) 是简单的,枚举 \(k\),因为线段树建树是线性的,所以直接线段树就好了。
- \(k>B\) ,莫队维护处每个询问区间内每个数是否出现的 \(bitset\) ,询问时暴力在$ bitset$ 跳就行了。复杂度应该是 \(O(m(\frac{n}{B}+\frac{n}{w}))\),不过好像跑的还挺快的。
直接这样分据说过不去,我们直接动态分,对于每个 \(k\) 算一下分在哪一种里优,然后就轻松过了。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+7;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
int n,m,A,M=0;
int a[N];
struct Query
{
int l,r,k,id;
}Q[N],q[N];
vector<int> qry[N];
bool tag[N];
int K,mn[N*4];
void build(int k,int l,int r)
{
if(l==r)
{
mn[k]=a[l]%K;
return;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
mn[k]=min(mn[k<<1],mn[k<<1|1]);
}
int ask(int k,int l,int r,int L,int R)
{
if(L<=l&&r<=R)return mn[k];
int mid=(l+r)>>1;
if(R<=mid)return ask(k<<1,l,mid,L,R);
if(L>mid)return ask(k<<1|1,mid+1,r,L,R);
return min(ask(k<<1,l,mid,L,mid),ask(k<<1|1,mid+1,r,mid+1,R));
}
int ans[N],blk[N],B;
bool cmp(Query x,Query y)
{
if(blk[x.l]!=blk[y.l])return x.l<y.l;
return x.r<y.r;
}
bitset<N> W;
int cnt[N];
inline void ins(int x)
{
cnt[x]++;
if(cnt[x]==1)W[x]=1;
}
inline void del(int x)
{
if(cnt[x]==1)W[x]=0;
cnt[x]--;
}
int main()
{
//freopen("a.in","r",stdin);
read(n);read(m);
for(int i=1;i<=n;i++)read(a[i]),A=max(A,a[i]);A++;
for(int i=1;i<=m;i++)
{
read(Q[i].l);read(Q[i].r);read(Q[i].k);
Q[i].id=i;
Q[i].k=min(Q[i].k,A);
qry[Q[i].k].push_back(i);
}
for(int i=1;i<=A;i++)if(qry[i].size())
{
if(qry[i].size()*A/i<=n)
{
for(int j:qry[i])tag[j]=1;
continue;
}
K=i;
build(1,1,n);
for(int j:qry[i])ans[j]=ask(1,1,n,Q[j].l,Q[j].r);
}
for(int i=1;i<=m;i++)if(tag[i])q[++M]=Q[i];
B=sqrt(n);
for(int i=1;i<=n;i++)blk[i]=(i-1)/B+1;
sort(q+1,q+M+1,cmp);
int l=1,r=0;
for(int i=1;i<=M;i++)
{
int u=q[i].id,k=q[i].k;
while(r<q[i].r)ins(a[++r]);
while(l>q[i].l)ins(a[--l]);
while(r>q[i].r)del(a[r--]);
while(l<q[i].l)del(a[l++]);
ans[u]=1e9;
for(int j=W._Find_first();ans[u]&&j<W.size();j=W._Find_next((j/k+1)*k-1))
if(j<W.size())ans[u]=min(ans[u],j%k);
}
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}
[POI2015] CZA
从小到大插入,注意到我们只关注每种空隙有多少个,并且只需要记录空隙两边的数与当前数的差。
因为 \(p\) 很小,我们暴搜出所有状态,大概只有二十多,然后跑个简单的 dp of dp 就行了。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+7;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
#define state vector<PII>
map<state,int> id;
int p,idx=0,te=0;
const int M = 27;
vector<PII> ers[M],cont[M];
int trans[M][M];
void put(state s)
{
cout<<s.size()<<endl;
for(auto u:s)printf("(%d,%d)",u.first,u.second);printf("\n");
}
int dfs(state seq)
{
sort(seq.begin(),seq.end());
if(id[seq])return id[seq];
id[seq]=++idx;
int x=idx;
cont[x]=seq;
state newstate;
for(auto u:seq)
{
u.first++;
u.second++;
if(u.second<=p&&u.first<=p)newstate.push_back(u);
else ers[x].push_back(u);
}
swap(seq,newstate);
for(int i=0;i<(int)seq.size();i++)
{
auto u=seq[i];
state now;
for(int j=0;j<(int)seq.size();j++)if(i!=j)now.push_back(seq[j]);
now.push_back(mk(u.first,0));
now.push_back(mk(0,u.second));
int y=dfs(now);
trans[x][y]++;
}
return x;
}
int n,m;
int dp[N][M];
map<PII,int> ban;
const int mod = 1e9+7;
int main()
{
read(n);read(m);read(p);
state now;now.push_back(mk(0,0));
int r=dfs(now);
for(int i=1;i<=m;i++)
{
int x,y;
read(x);read(y);
x=n-x;y=n-y;
ban[mk(x,y)]=1;
}
dp[0][r]=1;
for(int i=1;i<n;i++)
for(int j=1;j<=idx;j++)
if(dp[i-1][j])
{
bool flag=1;
for(auto u:ers[j])
{
auto o=mk(i-u.first,i-u.second);
if(ban.find(o)!=ban.end())
{
flag=0;
break;
}
}
if(!flag)continue;
for(int k=1;k<=idx;k++)if(trans[j][k])
dp[i][k]=(dp[i][k]+1ll*trans[j][k]*dp[i-1][j]%mod)%mod;
}
int ans=0;
for(int j=1;j<=idx;j++)if(dp[n-1][j])
{
bool flag=1;
for(auto u:cont[j])
{
auto o=mk(n-1-u.first,n-1-u.second);
if(ban.find(o)!=ban.end())
{
flag=0;
break;
}
}
if(!flag)continue;
ans=(ans+dp[n-1][j])%mod;
}
cout<<ans;
return 0;
}
[USACO21FEB] Minimizing Edges P
会不了一点。
首先,因为一条边可以来回走,所以只要确定了到每个点分别经过奇数/偶数条边的最短路长度,就可以确定 \(f_G(a,b)\) 。
首先,如果图是一个二分图,那么奇数和偶数最短路一定只存在一种,我们只需要保留图的一棵最短路树就可以满足了,否则每个点一定都有奇数和偶数最短路(因为可以去奇环绕一圈)。
对于一个点,设两种最短路中较小的为 \(a_i\),较大的为 \(b_i\) ,我们就把每个点记为 \((a_i,b_i),a_i<b_i\) 。
我们发现,对于每个 \(i\),一定至少存在以下两种之一:
- 存在与 \(i\) 相连的点 \(j\),满足 \((a_j,b_j)=(a_i-1,b_i-1)\) 。
- 存在与 \(i\) 相连的点 \(j,k\),满足 \((a_j,b_j)=(a_i-1,b_i+1),(a_k,b_k)=(a_i+1,b_i-1)\) 。注意有一个特殊情况,当 \(a_i=b_i-1\) 时,因为 \(a_i+1>b_i-1\) ,所以相当于是 \((a_k,b_k)=(a_i,b_i)\) 。
我们把 \((a_i,b_i)\) 相等的点放在一起,记录其个数,如果我们把所有 \((a_i,b_i)\) 扔平面上,那么发现我们可以一条一条主对角线考虑,因为第一种情况连边代价小于第二种,所以我们贪心的选,把所有点以 \(x+y\) 为第一关键字,\(x\) 为第二关键字从小到大排序,然后分类讨论:
-
如果不存在 \((a_i-1,b_i+1)\) ,那么就全部连接 \((a_i-1,b_i-1)\) 。
-
如果 \(a_i\neq b_i-1\) ,那么会有一些从 \((a_i-1,b_i+1)\) 延伸过来的边,记其个数为 \(c\),\((a_i,b_i)\) 个数为 \(t\) :
- 不存在 \((a_i-1,b_i-1)\),那么所有点都只能选择第二类边,有 \(\min (t,c)\) 个已经被连过了,需要再向 \((a_i-1,b_i+1)\) 连 \(\max(t,c)-c\) 条边,并且把 \(t\) 条边传给 \((a_i+1,b_i-1)\) 。
- 存在 \((a_i-1,b_i-1)\) , \(c<t\) ,那么还有 \(t-c\) 个点可以连给 \((a_i-1,b_i-1)\) ,然后把 \(c\) 条边传下去。
- \(c\geq t\),把 \(t\) 条边传下去。
-
如果 \(a_i=b_i-1\),其实是类似的,只不过我们不能传下去,而是可以在 \((a_i,b_i)\) 内部匹配。方便起见,我们先当成上一种做,求出需要传下去的个数 \(c'\),然后加上 \(\lceil \frac{c'}{2}\rceil\) 条边即可。
这也可以看出来,我们之所以要把之前的 \(c\) 条边传下去而不是连向 \((a_i-1,b_i-1)\),是因为后面有可能有 \(a_i=b_i-1\) 的特殊点,此时只需要一半的代价,因此一定更优。
用个 map
维护 ,复杂度 \(O(n\log n)\) 。
code
#include<bits/stdc++.h>
using namespace std;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 2e5+7;
int dis[N*2];
int n,m;
vector<int> G[N*2];
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
#define X(x) x.first
#define Y(x) x.second
struct node
{
int d,x,y,c;
}seq[N];
bool cmp(node A,node B)
{
if(A.d!=B.d)return A.d<B.d;
return A.x<B.x;
}
int rest[N];
int cl(int x,int y)
{
if(x%y==0)return x/y;
return x/y+1;
}
void solve()
{
read(n);read(m);
for(int i=1;i<=2*n;i++)G[i].clear(),dis[i]=1e9;
for(int i=1;i<=m;i++)
{
int x,y;
read(x);read(y);
G[x].push_back(y+n);G[y+n].push_back(x);
G[y].push_back(x+n);G[x+n].push_back(y);
}
queue<int> q;
q.push(1);
dis[1]=0;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int y:G[x])
{
if(dis[y]==1e9)
{
dis[y]=dis[x]+1;
q.push(y);
}
}
}
if(dis[1+n]==1e9)//bigraph
{
printf("%d\n",n-1);
return;
}
if(dis[1+n]==1)
{
printf("%d\n",n);
return;
}
map<PII,int> cnt;
for(int i=1;i<=n;i++)
{
int x=dis[i],y=dis[i+n];
if(x>y)swap(x,y);
cnt[mk(x,y)]++;
}
int idx=0;
for(auto u:cnt)seq[++idx]=(node){u.first.first+u.first.second,u.first.first,u.first.second,u.second};
sort(seq+1,seq+idx+1,cmp);
int ans=0;
for(int i=1;i<=idx;i++)
{
rest[i]=0;
int x=seq[i].x,y=seq[i].y;
int l=(cnt.find(mk(x-1,y+1))!=cnt.end());
int p=(cnt.find(mk(x-1,y-1))!=cnt.end());
int c=(y==x+1);
if(!p&&!l)
{
rest[i]=1;
ans+=1;
continue;
}
if(!l)
{
ans+=seq[i].c;
continue;
}
if(!p)
{
ans+=max(seq[i].c,rest[i-1])-rest[i-1];
rest[i]=seq[i].c;
}
else
{
if(rest[i-1]<seq[i].c)
{
rest[i]=rest[i-1];
ans+=seq[i].c-rest[i-1];
}
else rest[i]=seq[i].c;
}
if(c)ans+=cl(rest[i],2);
else ans+=rest[i];
}
printf("%d\n",ans);
}
int main()
{
int t;
read(t);
while(t--)
{
solve();
}
return 0;
}
[USACO20DEC] Cowmistry P
首先求出 \(B=2^d> K\) 的最小的 \(B\) 。
那么按照 \(B\) 对值域分块,不同块内显然不能同时选。
每个区间可以拆成若干整块和最多两个散块,整块的答案都是一样的,散块总共只有 \(O(n)\) 块,我们对每个散块分别处理,一个散块内我们不妨看成 \([0,B)\)。
设 \(D=2^{d-1}\),那么就有两个子区间 \([0,D),[D,B)\) ,每个子区间内的数任意异或一定都不超过 \(K\),这一部分方案数我们简单算即可,然后来看跨过区间的贡献,一定是一边选了一个数,另一边选了两个。我们对于一边的每个数,计算出另一边有多少数与他异或不超过 \(K\),然后把 \(\binom{c_i}{2}\) 求和即可。
我们考虑递归计算 \(solve(A,B,d,C)\),表示对于 \(A\) 内的每一个区间内的数,求出 \(B\) 里区间内有多少个数与它异或不超过 \(K\),记为 \(c_i\),然后把 \(\binom{c_i+C}{2}\) 求和,并且这些区间的高位全部相同,且当前值域区间都可以看成是 \([0,2^d)\) 。
- 若 \(A\) 为空,直接返回。
- 若 \(B\) 为空,则就把 \(\binom {C}{2}\) 乘上 \(A\) 里的数的个数。
- 若 \(B\) 是一个满的,也就是 \([0,2^d)\) 内的数都有,那么此时可以发现 \(A\) 内的每一个数的 \(c_i\) 都相同,简单算算就能返回。
- 否则,把 \(A,B\) 按照 \([0,2^{d-1}),[2^{d-1},2^d)\) 分裂成四个区间,然后可以类似数位 \(dp\) 一样的讨论继续递归下去,可以发现每个区间只会被递归一次。
算一下复杂度,对于 \(B\) ,类似于动态开点的值域线段树,所以复杂度 \(O(n\log V)\),对于 \(A\),因为它的返回条件没有 \(A\) 为满的情况,所以我们要再分析下:
- 不妨把 \(A\) 割成若干个线段树上的节点区间,那么这些区间上方的总复杂度一定不超过 \(O(n\log V)\) ,对于下方,因为区间不交,且只有 \(B\) 不为空时才有用,所以也不超过 \(O(n\log V)\) 。
综上,总复杂度 \(O(n\log V)\) 。
另外,本体可以看成 CF1616H 的加强版,只需要把上述过程改成 \(dp\) 就行了,并且直接从值域角度理解感觉要比 trie 树更直观。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar())f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 6e6+7;
const int mod = 1e9+7;
int Pow(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
const int i6=Pow(6,mod-2),i2=Pow(2,mod-2);
int n,m,R;
int lp[N],rp[N];
inline int C3(int x){return 1ll*x*(x-1)%mod*(x-2)%mod*i6%mod;}
inline int C2(int x){return 1ll*x*(x-1)/2%mod;}
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
#define segment vector<PII >
map<int,segment > vec;
int B=1,ans=0;
int suan(segment L)
{
int res=0;
for(auto u:L)
res=(res+(u.second-u.first+1)%mod)%mod;
return res;
}
void calc(segment A,segment B,int P,int c)
{
if(A.empty())return;
if(B.empty()){ans=(ans+1ll*suan(A)*C2(c)%mod)%mod;return;}
segment F={mk(0,P-1)};
if(B==F)
{
c=(c+((P-1)&m))%mod;
ans=(ans+1ll*suan(A)*C2(c)%mod)%mod;
return;
}
segment a[2],b[2];
P/=2;
auto apply=[&](segment &S,PII E)
{
E.first=max(E.first,0);
E.second=min(E.second,P-1);
if(E.first<=E.second)S.push_back(E);
};
//split
for(auto S:A) apply(a[0],S),apply(a[1],mk(S.first-P,S.second-P));
for(auto S:B) apply(b[0],S),apply(b[1],mk(S.first-P,S.second-P));
if(m&P)
{
calc(a[0],b[1],P,(c+suan(b[0]))%mod);
calc(a[1],b[0],P,(c+suan(b[1]))%mod);
}
else
{
calc(a[0],b[0],P,c);
calc(a[1],b[1],P,c);
}
}
void solve(segment E)
{
int D=B/2;
int c0=0,c1=0;
segment S[2];
for(auto u:E)
{
int l=u.first,r=u.second;
if(l/D==r/D)S[l/D].push_back(mk(l%D,r%D));
else
{
S[0].push_back(mk(l%D,D-1));
S[1].push_back(mk(0,r%D));
}
}
for(int c=0;c<=1;c++)
{
ans=(ans+C3(suan(S[c])))%mod;
calc(S[c],S[c^1],D,0);
}
}
int main()
{
//freopen("a.in","r",stdin);
read(n);read(m);++m;
for(int i=1;i<=n;i++)read(lp[i]),read(rp[i]);
B=1;
while(B<=m)B<<=1;m-=(B/2);
int cnt=0;
for(int i=1;i<=n;i++)
{
int l=lp[i],r=rp[i];
if(l/B==r/B)vec[l/B].push_back(mk(l%B,r%B));
else
{
int pl=((l/B)+1)*B;
int pr=(r/B)*B;
vec[l/B].push_back(mk(l%B,(pl-1)%B));
vec[r/B].push_back(mk(pr%B,r%B));
cnt+=(r/B)-(l/B)-1;
cnt%=mod;
}
}
for(auto u:vec)solve(u.second);
int c=(1ll*B*C2(m)%mod+2ll*C3(B/2)%mod)%mod;
ans=(ans+1ll*c*cnt%mod)%mod;
cout<<ans;
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步