AGC 选做
Submit
AGC 非常恐怖。
这篇题解跨越了两年,些代码甚至跨越了四年,且还不知道多久能结束。
选做内容:AGC 难度>2800的部分,包含几个橙题,然后去掉所有我不会的题
题解进度:133/157
目前长度:751 KB(支持来要source)
因为有些题太恐怖了,之后可能会将这篇题解分成 Easy 和 Hard 两部分 咕了,完整列表挺好
部分题目包含极长的证明,您可以选择跳过证明或者帮我检查证明正确性(bushi)
bonus:看到这句话的人可以来催更题解
Contest ID | |||||
---|---|---|---|---|---|
AGC 058 | F | ||||
AGC 057 | C | D | E | F | |
AGC 056 | B | D | E | F | |
AGC 055 | C | D | F | ||
AGC 054 | D | E | |||
AGC 053 | C | D | E | ||
AGC 052 | C | D | E | F | |
AGC 051 | C | D | E | F | |
AGC 050 | D | E | |||
AGC 049 | D | E | |||
AGC 048 | D | E | F | ||
AGC 047 | D | E | F | ||
AGC 046 | D | E | F | ||
AGC 045 | B | C | D | E | F |
AGC 044 | C | D | E | F | |
AGC 043 | C | D | E | F | |
AGC 041 | D | E | F | ||
AGC 040 | D | E | |||
AGC 039 | D | E | F | ||
AGC 038 | E | F | |||
AGC 037 | D | E | F | ||
AGC 036 | D | E | F | ||
AGC 035 | C | D | E | F | |
AGC 034 | D | E | F | ||
AGC 033 | D | E | F | ||
AGC 032 | C | D | E | F | |
AGC 031 | D | E | F | ||
AGC 030 | D | E | F | ||
AGC 029 | E | F | |||
AGC 028 | C | D | E | F | F2 |
AGC 027 | D | E | F | ||
AGC 026 | D | E | F | ||
AGC 025 | D | E | F | ||
AGC 024 | D | E | F | ||
AGC 023 | D | E | F | ||
AGC 022 | D | E | F | ||
AGC 021 | E | F | |||
AGC 020 | D | E | F | ||
AGC 019 | E | F | |||
AGC 018 | E | F | |||
AGC 017 | C | E | F | ||
AGC 016 | D | E | F | ||
AGC 015 | E | F | |||
AGC 014 | E | F | |||
AGC 013 | E | F | |||
AGC 012 | E | F | |||
AGC 011 | E | F | |||
AGC 010 | E | F | |||
AGC 009 | D | E | |||
AGC 008 | E | F | |||
AGC 007 | C | E | F | ||
AGC 006 | D | E | F | ||
AGC 005 | E | F | |||
AGC 004 | E | F | |||
AGC 003 | E | F | |||
AGC 002 | E | F | |||
AGC 001 | E | F |
现在正式开始。
AGC058F Authentic Tree DP
Problem
对于一棵树 ,定义 为:
- 如果 只有一个点,则
- 否则,对于 的每一条边 ,记 为删去 后形成的两棵树,则
给定一棵 个点的树 ,求出 ,答案模
Sol
显然dp不能直接做,考虑描述这个dp的组合意义。
可以发现这个过程类似于随机删边,但随机删边是除以 ,这里是除以 。一种尝试方式是加一条边,但因为每个连通块只能加一条边,删边形成两个连通块后的情况难以处理。
可以发现除以 相当于随机选点,考虑将两者联系起来,可以发现如下结果:
考虑给树任意定根,然后随机选择一个点,如果选到根就失败,否则删去选择点和父亲的边,对两个连通块分别做。
那么把 看成成功概率,上述过程就描述了原先的dp。可以发现不同连通块的随机选择可以放到一起做:在两个连通块的点集中共同随机一个点,在对应连通块中操作,这样不改变概率。那么操作可以看成如下形式:
重复如下过程,直到终止:
- 如果当前所有连通块都是单点,则成功。否则随机一个点 ,然后:
- 如果 所在连通块为单点,则不进行操作,否则:
- 如果 是所在连通块的根,则失败,否则:
- 如果 不是所在连通块的根,则删去 与父亲的边。
通过之前的分析可以发现 等于这一过程成功的概率。
考虑算成功的概率。一个点被选到一次后它就成为了当前连通块的根,因此每个点只有前两次操作有意义:第二次选到它且合法后它就没用了。那么只关心前两次选到每个点,这样事件变为有限个,也不需要考虑终止,可以发现成功当且仅当:
- 对于非根节点,它第二次被选到时,其所有儿子都被选到过一次。
但根第一次被选到前就是根,因此有:
- 对于根,它第一次被选到时其它点必须都被选到过。
但直接考虑每个点的前两次操作还是困难的:如果只考虑第一次操作,则 种序列出现概率相同。而一种 个事件的序列出现的概率可以算,但是涉及到序列整体,难以处理。
可以发现对于第二次被选,这个过程只用到了每一个点第二次被选到的时间晚于一些点第一次被选到。那么不同点第二次被选到的事件之间不存在影响。考虑先决定所有点第一次被选的顺序,接下来合法的概率一定是每个点合法概率的乘积。
每一条限制都只涉及到一条边上的两个点,考虑树形dp处理所有点第一次被选到的顺序。设 表示只考虑 子树,子树内合法且 是第 个被选到的概率。考虑转移,限制是 第二次被选到的时间晚于每一个儿子第一次被选到的时间。因此考虑先合并儿子,只需要知道所有儿子中最后一个第一次被选到的在所有子树内排在第几个。考虑合并两个子树的过程,相当于两个子树内的点排成两列,每一列中有一个选中的点(表示之前根在所有点中的位置),然后随机归并两个序列,保留选中点中靠后的一个。这是一个较为简单的转移:枚举保留的点属于哪一侧,由对称性只分析一侧的情况:枚举这一侧选中点的位置,枚举另一侧有多少个点插入到了选中点之前,那么另一侧合法的概率是 的前缀和(另一个选中点在前面),然后这样归并的概率是方案数(选中点两侧分别归并)除以总方案数,即形如:
可以发现复杂度是两侧子树大小乘积,因此是总共 的。
然后考虑子树的根节点。限制是根第二次出现晚于所有儿子的最后一个第一次出现。考虑将根的第一次出现插入合并好的子树内其它点。设子树总共有 个点,考虑根第一次被选在第 个,所有儿子中最后一个第一次出现在第 个时合法的概率(子树外的部分和这一限制独立,可以不考虑)。显然 一定合法,否则考虑第一次选了根后,相当于之后还有一些点顺序首次出现,根下一次出现的时间不早于之后某个点首次出现的概率。此时之前出现过的都可以不考虑,可以假装根还没有出现,相当于问从当前位置向后,只考虑根和本来在之后首次出现的点,求根(在之后)首次出现的时间不早于前 个点首次出现的概率,可以发现答案是(总元素数量 )除以总元素数量,在原问题中即为 。
原转移是将根的首次出现插入序列,但这也容易处理:与之前类似, 时 不受插入影响,系数仍然是 ,否则 会增加 。因此在插入转移时,设本来儿子最后一次出现在第 个位置,根插入后在第 个位置,那么转移系数仍然只和 的大小关系有关,且每一部分的系数可以看成两个分别只和 有关的系数的乘积。前后缀和处理即可。这样完成了所有的转移。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5050
#define mod 998244353
int n,a,b,head[N],cnt,c[N][N],inv[N];
int sz[N],dp[N][N],s1[N],s2[N],rs[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
struct edge{int t,next;}ed[N*2];
void adde(int f,int t)
{
ed[++cnt]=(edge){t,head[f]};head[f]=cnt;
ed[++cnt]=(edge){f,head[t]};head[t]=cnt;
}
void dfs(int u,int fa)
{
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)
{
dfs(ed[i].t,u);
if(!sz[u])for(int j=1;j<=sz[ed[i].t];j++)dp[u][j]=dp[ed[i].t][j];
else
{
for(int j=1;j<=sz[u];j++)s1[j]=(s1[j-1]+dp[u][j])%mod;
for(int j=1;j<=sz[ed[i].t];j++)s2[j]=(s2[j-1]+dp[ed[i].t][j])%mod;
for(int j=1;j<=sz[u];j++)for(int k=1;k<=sz[ed[i].t];k++)
rs[j+k]=(rs[j+k]+1ll*dp[u][j]*s2[k]%mod*c[j+k-1][j-1]%mod*c[sz[u]+sz[ed[i].t]-j-k][sz[u]-j])%mod,
rs[j+k]=(rs[j+k]+1ll*dp[ed[i].t][k]*s1[j]%mod*c[j+k-1][k-1]%mod*c[sz[u]+sz[ed[i].t]-j-k][sz[u]-j])%mod;
int ir=pw(c[sz[u]+sz[ed[i].t]][sz[u]],mod-2);
for(int j=1;j<=sz[u]+sz[ed[i].t];j++)dp[u][j]=1ll*rs[j]*ir%mod,rs[j]=0;
}
sz[u]+=sz[ed[i].t];
}
if(u==1)
{
int ir=pw(sz[u]+1,mod-2);
for(int j=1;j<=sz[u];j++)as=(as+1ll*dp[u][j]*(sz[u]+1-j)%mod*ir)%mod;
return;
}
int su=0;
for(int i=1;i<=sz[u];i++)su=(su+dp[u][i])%mod,rs[i+1]=(rs[i+1]+su)%mod;
su=0;
for(int i=sz[u];i>=0;i--)rs[i+1]=(rs[i+1]+1ll*inv[sz[u]-i+1]*su)%mod,su=(su+1ll*dp[u][i]*(sz[u]-i+1))%mod;
if(!sz[u])rs[1]=1;
sz[u]++;
int ir=pw(sz[u],mod-2);
for(int i=1;i<=sz[u];i++)dp[u][i]=1ll*rs[i]*ir%mod,rs[i]=0;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<=n;i++)c[i][0]=c[i][i]=1,inv[i]=pw(i,mod-2);
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),adde(a,b);
dfs(1,0);
printf("%d\n",as);
}
AGC057C Increment or Xor
Problem
给一个 的排列 ,你可以对它进行如下两种操作:
- 将每个 变为
- 选择一个 ,将每个 变为
你需要使得 。判断是否有解,如果有解构造一组操作次数不超过 的解。
Sol
考虑如何描述 操作,一种经典想法是使用反向插入的 01-Trie。
考虑将所有值在二进制下翻转后插入 01-Trie,考虑一次 操作的影响,它会把二进制表示末尾的一段 进位为 ,可以发现这在当前的 Trie 上相当于如下操作:
找到当前值为 的点到根的链(即根一直向右的链),从下往上交换这条链上的两个儿子。(这也可以看成将当前一直向右的链变为一直向左,其余子树内情况不变)。
然后考虑异或操作,这显然相当于选择 Trie 的一些层,对于这些层的所有点交换它的两个儿子。
考虑在 Trie 的每个叶子上写上这个元素原来的位置,那么问题相当于有两个这样的 Trie,你需要通过上述操作,使得它们满足对应叶子上权值相等。
注意到操作都是若干次交换一个点的两个儿子,因此如果不能通过任意交换两个儿子达到目标则一定无解。考虑重排权值使得目标 Trie 上叶子权值依次为 ,则可以通过任意交换两个儿子达到目标当且仅当初始 Trie 上任意一个节点子树中的叶子权值构成一个连续段。可以 dfs 一次判断是否合法,这时还可以求出每个点是否需要交换儿子,可以发现只有唯一一种每个点是否交换儿子的方式合法。
接下来只需要判断是否能使用上述两种操作完成目标。这里可能有多种做法。
考虑能否让第一种操作任意指定一条链,可以发现如果将 看成一个整体,则这个整体操作相当于先将当前值为 的链移到最右侧,再翻过来,再将其它点翻回去,因此这相当于对于任意一条链,从下往上交换每个点的两个儿子。(场上写的是 ,然后得到了略为复杂的过程)
设每个点是否交换儿子对应一个权值 ,则上述操作相当于一个叶子到根的链反转(但不包括叶子本身,因为叶子没有儿子),第二种操作相当于一层整体反转。显然有解当且仅当能用这两种操作得到解。
一种想法是枚举倒数第二层是否操作,这之后可以用这一层的情况得到每条链是否操作(这一层每个点的两个叶子节点操作等效),然后只需要考虑上面能否通过操作层达到目标。
但还可以发现倒数第二层中所有点都选一个叶子节点进行操作时,得到的效果相当于将最后一层翻转一次。因此可以钦定最后一层不翻转,然后做上面的过程。
最后考虑构造方案,需要注意的是前面构造的操作会改变节点顺序,因此需要维护前面操作的结果以得到此时每个点的位置。
复杂度 或 ,操作次数不超过 ,合并 可以做到 ,还可以选择最后一层翻转的方式做到 。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 530001
int n,a,fi,si[N],ci,vi;
int p[N],rid[N],is[N],is2[N];
struct sth{int l,r;};
sth operator +(sth a,sth b){return (sth){min(a.l,b.l),max(a.r,b.r)};}
sth dfs(int x,int l,int r)
{
if(l==r)return (sth){p[l],p[l]};
int mid=(l+r)>>1;
sth sl=dfs(x<<1,l,mid),sr=dfs(x<<1|1,mid+1,r);
if(sl.l>sr.l)is[x]=1;
sth tp=sl+sr;
if(tp.r-tp.l>r-l)fi=1;
return tp;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<1<<n;i++)
{
int r=0;
for(int j=1;j<=n;j++)if((i>>j-1)&1)r|=1<<n-j;
rid[r]=i;
}
for(int i=0;i<1<<n;i++)scanf("%d",&a),p[rid[a]]=rid[i];
dfs(1,0,(1<<n)-1);
for(int i=1<<n-1;i<1<<n;i++)if(is[i])
{
si[++ci]=i*2-(1<<n);
for(int j=i;j;j>>=1)is[j]^=1;
}
for(int i=1;i<1<<n;i<<=1)
{
for(int j=i;j<i*2;j++)if(is[j]!=is[i])fi=1;
if(is[i])vi|=i;
}
if(fi)printf("No\n");
else
{
printf("Yes\n%d\n",ci*2+1);
int ls=0;
for(int i=1;i<=ci;i++)
{
int nw=1,s1=1,v1=si[i];
for(int j=1;j<n;j++)
{
int tp=is2[s1]^((v1>>n-j)&1);
is2[s1]^=1;
nw=nw*2+tp;s1=s1*2+((v1>>n-j)&1);
}
int as=((1<<n)-1)^rid[nw*2-(1<<n)];
printf("%d -1 ",ls^as);ls=as;
}
printf("%d\n",vi^ls);
}
}
AGC057D Sum Avoidance
Problem
给定正整数 ,称一个正整数集合 是好的,当且仅当它满足如下条件:
- 中元素在 之间。
- 不存在 以及 使得 ,即你不能用 中元素多次相加得到 。
考虑所有好的集合中集合大小最大的一个,如果有多个,考虑将集合从小到大写成序列后,字典序最小的一个,记它为 。
多组询问,每次询问 中第 小的元素,你需要输出它的值或者输出 。
Sol
考虑好的集合的性质。首先,显然 中最多有一个在 中,如果 为偶数则 不能在 中。这样 的大小不能超过 。
同时,考虑选择大于 的所有数,它们任意两个相加都大于 ,因此它们构成一个好的集合。因此存在大小为 的好集合。
考虑大小为 的好集合的性质,根据上一条可以发现对于任意满足 的正整数 , 中必须正好有一个在 中。
考虑 中所有小于 的元素,设它们构成集合 。考虑 的性质,可以得到如下结果:
- 如果 且 ,则 。
如果这一结论不成立,则 ,因此 都在 中,矛盾。因此结论成立。
考虑满足这一性质的集合 以及对应的 ,如果这个集合不合法,即集合中有一些数能组合出 ,如果组合出 的数中有一个大于 的数,则可以推出它不满足上一个性质,从而只能是 中的数组合出了 。因此 还需满足如下限制:
- 不存在 以及 使得
可以发现,如果 满足这两条限制,则 对应的 是合法的,且这是充分必要的。
考虑如何构造字典序最小的 ,可以发现只要从小到大考虑每个数,尝试将它加入 ,则得到的 对应的 一定是需要的 。
那么加入数时有两种情况:
- 当前数可以被之前 中的数组合出。则它必须加入 ,显然加入它不会违反第二个条件。
- 当前数不能被 中数组合出,则如果加入它后不违反第二个条件,就会选择加入它。
考虑加入 的第一个数,记为 ,它一定是最小的与 互质的数。那么如果 ,则 。因此对于每个 ,最多有一个 的数使用第二种方式加入,因此第二种加入的数不超过 个。
考虑对于每个 记录 表示当前 中 的数中能组合出的 的数中的最小值。
则 满足这两个性质:第一种加入的数不影响 ,可以使用 还原出 。
因此考虑支持找到下一个不能被 组合出且可以加入的数,进行不超过 次即可得到 ,从而得到答案。
考虑加入一个这样的数 ,则一定有 。考虑加入后 的变化,枚举加入的数用了多少次,可以得到如下结果:
一个数能被加入当且仅当 且加入后 (第一个条件)。考虑枚举 ,则由加入后 的表达式,可以得到这种情况合法当且仅当 大于等于某个值。从而可以得到这个 下当前第一个能加入的数(或者不存在)
那么每次可以对于每个 求出结果,这样即可得到接下来第一个能使用第二种方式加入的元素。单次复杂度 ,总复杂度 。
这样即可得到 ,一个数 当且仅当 且 。
此时对于一个权值 ,容易 求出 中小于等于 的数个数,因此二分即可得到 中第 小的元素。如果答案大于 ,则反过来类似二分即可。
最后考虑 的范围,可以发现 的 大于 ,因此 。事实上 。
复杂度
注意可能的细节,比如 的情况。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 49
#define ll long long
int T;
ll n,k,m,ds[N],st[N],ct;
ll solve(ll n,ll k)
{
if(k>(n-1)/2)return -1;
if(n==3)return 2;
if(n==4)return 3;
if(n==6)return k+3;
m=1;while(n%m==0)m++;
for(int i=1;i<m;i++)ds[i]=2e18;
st[ct=1]=m;
while(1)
{
ll rs=2e18;
for(int i=1;i<m;i++)
{
ll rb=0;
for(int j=1;j<=m;j++)
{
int ri=(n+m*m-i*j)%m;
rb=max(rb,(n-ds[ri])/j+1);
}
if((rb+m-i)%m)rb+=m-(rb+m-i)%m;
if(rs>rb&&rb<ds[i])rs=rb;
}
if(rs>n/2)break;
st[++ct]=rs;
while(1)
{
int fg=0;
for(int j=0;j<m;j++)
{
int ri=(j+rs)%m;
if(ds[ri]>ds[j]+rs)ds[ri]=ds[j]+rs,fg=1;
}
if(!fg)break;
}
}
ll lb=1,rb=n/2,as=-1;
while(lb<=rb)
{
ll mid=(lb+rb)/2,su=0;
for(int i=0;i<m;i++)if(mid>=ds[i])
su+=(mid-ds[i])/m+1;
if(su>=k+1)as=mid,rb=mid-1;
else lb=mid+1;
}
if(as!=-1)return as;
k=(n-1)/2+1-k;
lb=1,rb=n/2,as=-1;
while(lb<=rb)
{
ll mid=(lb+rb)/2,su=0;
for(int i=0;i<m;i++)if(mid>=ds[i])
su+=(mid-ds[i])/m+1;
if(mid-su+2>=k+1)as=mid,rb=mid-1;
else lb=mid+1;
}
return n-as;
}
int main()
{
scanf("%d",&T);
while(T--)scanf("%lld%lld",&n,&k),printf("%lld\n",solve(n,k));
}
AGC057E RowCol/ColRow Sort
Problem
有一个 的矩阵 , 满足每行单调不降,每列单调不降。
求有多少个 的矩阵 满足如下条件:
- 将 的每一行排序,再将 的每一列排序,可以得到 。
- 将 的每一列排序,再将 的每一行排序,可以得到 。
答案模
Sol
考虑 的情况,此时矩阵中只有 。记 表示 的第 行中 的数量, 表示 的第 列中 的数量。类似定义 。
考虑先排序行再排序列的过程,排序行后第 行只有最后 个是 。再排序列可以发现相当于将 进行排序。因此这种情况满足要求当且仅当 构成的可重集与 构成的可重集相同。
类似的,满足另外一个条件当且仅当 构成的可重集与 构成的可重集相同。
进一步考虑,可以得到如下性质:
在 问题中,对于一组合法方案,一定可以通过若干次交换行列的操作使得 。
证明:考虑每次删一个全 或者全 的行列,由 的性质,这样一定可以删空。而 的可重集相等, 的可重集相等,因此删除 时删 的对应行列即可,这样就得到了一个行列间的对应关系。
对于一般的情况,不能发现合法当且仅当对于每一个 ,将 的看成 ,剩余的看成 后得到的 问题满足上述条件。
那么一组合法的方案可以看成在所有位置中选择一些 的位置满足条件,再从这些位置中选择一些 的位置满足对应条件,以此类推。
此时一个问题是之前选择的位置会影响之后的选择,因此不能分开计算。但上一个问题有一个性质:可以交换行列使得 。同时重排行列不影响合法的条件,从而对于任意一个 ,在选择了 中 的位置后,可以将它们重排使得所有 的位置正好是 中所有 的位置,然后再选 的位置,进行之后的判定。
这样每一步的选择是相同的,因此方案数是每一步方案数的乘积。因此只需要解决 次如下问题(这里从交换行列的角度考虑):
有一个矩阵,当前矩阵中有一些 ,且每行每列的 都是一段后缀。
你可以任意进行交换行列。但当前矩阵中有一条从左下到右上,且只向右向上走的折线 ,需要满足交换后所有的 都在折线下方。求合法的方案数。
如果只能重排行,则只需要满足每一行换过去之后不超过折线 ,因此每一行能在的位置是一段后缀。这时的方案数容易求出,设第 行能被放到后 行,则由原来 的位置可以发现 不增,因此方案数为 。
回到原问题,考虑先重排列再重排行。在重排列之后,一行内的情况不会改变,而限制为没有 在折线上方,因此对于一行,可以找到从左往右第一个 ,将右侧的位置全部看成 ,这样不改变之后重排行问题的方案数。
这时可以发现所有的 满足每行每列的 构成一段后缀,因此 的边界为一条左下到右上的折线,设其为 。可以发现之后交换行的方案数只和 有关。
考虑一条折线 的贡献,相当于重排列得到 的方案数乘上看成 下面全部为 ,在此基础上重排行,不超过折线 的方案数。
首先考虑后者,由之前的分析可以得到,这里的方案数为从下往上考虑每一行,(这一行能放入的位置数减去之前的行数)的乘积,这个乘积可以在折线 每一步向上(加入行)时考虑。
然后考虑前者。由上述操作过程可以得到列的重排需要满足如下条件:
设 中第 列的高度为 ,重排列后第 列有 个 ,则 。
这等价于如下限制:
- 重排后所有的 不超过 。
- 对于每个 向上的位置,下一次 向右时,对应的列中 的数量必须和 当前的高度相等。
每一列可以被放进一段后缀。因此从左向右考虑 的每一列,仍然可以依次决定哪一列被放进来,且每一步独立。对于第二种情况的列,它的方案数是这种高度的列数量,对于其它情况方案数为高度小于当前高度的列数减去之前放过的列数量。
因此两种贡献都可以在折线向右上的每一步中考虑,贡献几乎只和当前这一步有关,只有第二部分需要考虑上一步是否是向上。
因此设 表示当前折线在 ,上一步是向上还是向右时,前面所有折线在前面部分的贡献和。转移时枚举下一步的方向,可以 计算系数。因此可以在 内得到答案。
注意这里都是将行列作为可区分的,而实际上 相同的行, 相同的列不可区分,因此需要乘上若干个 。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1505
#define mod 998244353
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int n,m,fr[N],ifr[N],v[N][N],as=1;
int dp[N][N][2],sx[N],sy[N];
int solve(int d)
{
int as=1;
for(int i=0;i<=m;i++)for(int j=0;j<=n;j++)dp[i][j][0]=dp[i][j][1]=0;
for(int i=0;i<=m;i++)sy[i]=0;
for(int i=1;i<=n;i++)
{
int tp=0;
for(int j=1;j<=m;j++)tp+=v[i][j]>=d-1;
sy[tp]++;
}
for(int i=m;i>=0;i--)as=1ll*as*ifr[sy[i]]%mod;
for(int i=m;i>=1;i--)sy[i-1]+=sy[i];
for(int i=0;i<=n;i++)sx[i]=0;
for(int i=1;i<=m;i++)
{
int tp=0;
for(int j=1;j<=n;j++)tp+=v[j][i]>=d;
sx[tp]++;
}
for(int i=n;i>=0;i--)as=1ll*as*ifr[sx[i]]%mod;
for(int i=1;i<=n;i++)sx[i]+=sx[i-1];
dp[0][0][0]=1;
for(int i=0;i<=m;i++)
for(int j=0;j<=n;j++)
{
dp[i][j+1][0]=(dp[i][j+1][0]+1ll*(dp[i][j][0]+dp[i][j][1])*(sy[m-i]-j))%mod;
dp[i+1][j][1]=(dp[i+1][j][1]+1ll*dp[i][j][1]*(sx[j]-i))%mod;
dp[i+1][j][1]=(dp[i+1][j][1]+1ll*dp[i][j][0]*(sx[j]-(j?sx[j-1]:0)))%mod;
}
return 1ll*as*(dp[m][n][0]+dp[m][n][1])%mod;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&v[i][j]);
fr[0]=1;for(int i=1;i<=1500;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[1500]=pw(fr[1500],mod-2);for(int i=1500;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
for(int i=1;i<=10;i++)as=1ll*as*solve(i)%mod;
printf("%d\n",as);
}
AGC057F Reflection
Problem
数轴上有三枚石子,它们的坐标都是整数,初始位置为 。你可以进行如下操作:
设当前三枚石子的坐标排序后为 。你可以选择如下两种操作之一:
- 将 对应石子移动到当前位置对于 的对称位置,即令 。
- 将 对应石子移动到当前位置对于 的对称位置,即令 。
求你可以得到多少不同的石子摆放状态。这里石子是不可区分的,即你需要统计可以达到的可重集 数量。答案模
多组数据
Sol
考虑相邻两个石子的距离,即令 。考虑操作对于 的变化。
设当前 ,则如果操作 ,状态会变为 ,否则状态会变为 。
考虑反过来的情况,即初始为 ,如果操作 ,状态会变为 ,否则状态变为 。
可以发现如果看成 也是无序的,则每一步操作都是一步辗转相减的过程,因此看成无序对后, 的状态只和操作次数有关。
从上面的例子还可以发现,对于一种无序对 ,最后实际上是 还是 只和最后一次操作是操作 还是操作 有关。
以及 可以唯一确定石子的位置,因此再考虑操作对于 的变化。
设当前状态为 ,分析一下可以发现如果操作 ,则 会减少 ,如果操作 则 会增加 。
因此每一步操作可以选择使 增加或减少一个值,且这个值之和当前的操作次数有关,和之前的操作无关。
考虑固定一个操作次数,需要求出在前面的操作中可能得到多少种不同的 。
考虑辗转相除的过程,不同的 只有 段,因此可以看成如下问题:
给定 (这里 对应权值, 对应出现次数)。求有多少个 满足如下条件(这里将向左看成不变,向右看成加对应值,可以发现在只有一个初值的情况下这样不改变结果):
根据辗转相除的性质,这里一定有 且 。
一个问题是不同的 可能得到同一个 ,需要考虑如何去重的问题。
考虑完整的辗转相除的过程,即上面的序列中最后一步满足 。则 都是 的倍数,不妨设 。
这可以看成一个扩展形式的斐波那契表示,因此可以尝试得到类似结论。
此时有如下性质:
证明:对于左侧的结果,考虑 即可证明。对于右侧的结果,容易得到 ,再使用 即可。
使用这一性质,可以得到如下结果:
- 在这种情况下,可以表示 中的任意整数,且可以使用从前向后贪心取最大的 的方式表示出任意一个整数。
证明:考虑从后往前归纳每个后缀,再利用上一个结果的左侧部分即可得到。
考虑对其使用贪心得到的 表示一个数,则可以得到如下结论:
- 一种 是一个数贪心得到的序列当且仅当不存在 满足 且满足 或 之一。
证明:充分性显然,只需要证明如果一个序列不是贪心得到的,则它满足右侧的情况。(为了简便, 的情况可以看成 ,两种讨论类似)
设 , 为第一个 的位置且 。由结论 的右侧,一定有 。考虑将多出的一个 退位,则有:
考虑将 放到左侧,剩余放到右侧,则由上一个引理可以得到右侧当前值小于 ,因此 。则 。
如果 ,则考虑再向下推一层,变为 ,这时一定有 ,再向下一层可以得到 ,此时满足条件。
如果 ,考虑下一位的情况,如果 则直接满足条件,否则变为 ,可以发现这是一个和上述问题形式相同的问题。那么对 归纳即可证明结论。
这个性质同时说明,如果两种不同的 得到了同一个和,则一定存在一个 满足存在一个 使得 且满足 或 之一。
现在考虑之前的问题,即不完整的辗转相除过程。假设当前最后一段不满(即 小于完整的辗转相除中的这一段 ),类似地,可以发现如下性质:
- 如果一种 满足存在一个 使得 且 ,则可以通过类似进位的方式找到一种不满足这个且和不变的 。
- 如果两种 得到了相同的和,则一定有一个 满足这一限制。
因此如果只统计不同的和的数量,则只需要统计满足不存在 使得 且 的 数量。
但还需要考虑最后一次操作是否为增加的操作,这与最后一段中 是否与 相同有关。
由上一条可以得到,满足这个条件的 一定可以通过若干次退位操作( 减一, 加 , 加一)得到任意一个与它得到的和相同的 。
考虑当前最后一个 的位置,可以发现退位操作只可能不改变最后一个 的位置,或者将其向后移动两位(这里的特例是 )。不考虑特例的情况下,可以得到如下结论:
一组满足条件的 可以操作得到一个满足 的方案当且仅当原先 ,可以得到一个 的方案当且仅当原先最后一个 的位置满足 奇偶性相同。
现在考虑一个固定操作步数的问题。限制是不能连续出现 。考虑相邻两个位置满足 ,如果序列的前面存在一个位置使得 ,考虑找到左侧第一个这样的位置 ,如果 则显然不合法,否则 ,也不合法。因此限制可以改写为如果相邻两个位置满足 ,则 。
这样可以得到 ,设 表示考虑了前 段,前面部分满足要求, 代表 , 代表 且前面存在一个 ( 的情况一定只有一种,不用考虑)。
但还需要考虑最后一步的问题,因此需要记录是否 以及上一个 位置的奇偶性。因此将状态变为 , 表示 且上一个 的位置 与 奇偶性相同, 表示上一个 的位置 与 奇偶性不同, 表示之前的 状态。转移考虑 取 或者其它值即可。
现在考虑将不同操作步数一起计算,可以发现不同的操作步数相当于取变化序列的一段前缀。即取 ,然后取新的 为 ,其中 。那么可以对整个序列 ,在 到第 段时,求出所有取了第 段一部分的问题()的答案和。这些问题前面的转移相同,最后一段转移需要考虑 的情况,可以对于每种情况 求和所有这些问题的转移系数和,即可求出它们的贡献。(注意计算初始状态)
最后还需要处理结尾的边界问题。一个问题是,上述讨论都认为当前两侧距离 ,但在最后一次操作前有 ,因此考虑将这一次操作单独处理。这样也可以避免上述讨论中做法需要特殊处理结尾的问题。
考虑这一次操作前的情况,此时序列 满足 ,以及(不妨设)。此时最后一段少了一位,但减去这一位后仍然有 ,此时仍然可以证明可以取到 间的所有数。
然后考虑这一步操作之后的情况,一个状态 可以移动到 ,相当于中心位置可以到 或 ,另外一个棋子可以在左侧或者右侧。
考虑此时中心位置的情况,之前的讨论将 变为了 ,如果变回去的话,可以发现中心可以取到 间的所有数,即能取到这里面所有同一个奇偶性的数。那么可以发现它们再 或 ,可以得到 个数,因此最后一部分的贡献是 。加上前面部分贡献即可。
还需要注意步数很少的情况,例如 ,这时一些实现可能出问题需要特判。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 159
#define mod 998244353
#define ll long long
int T,dp[N][3],ct;
ll a,b,c;
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
int solve(ll a,ll b)
{
if(a>b)a^=b^=a^=b;
if(!a)return 1+(!!b);
ll g=gcd(a,b);a/=g,b/=g;
for(int i=1;i<=ct;i++)for(int j=0;j<4;j++)dp[i][j]=0;ct=0;
ll as=0,ri=1;
while(a)
{
ll tp=b/a;
b%=a;a^=b^=a^=b;
if(!a)tp--;if(!tp)break;
ct++;ri+=b*tp;
if(!a)tp--;if(!tp)break;
int s0=tp%mod,s1=(tp-1)%mod*(tp%mod)%mod*(mod+1)/2%mod;
as=(as+(2ll*s0+2ll*s1)*(1+(ct>1)))%mod;
as=(as+1ll*(dp[ct-1][0]+dp[ct-1][1])*(2ll*s1+s0))%mod;
as=(as+(dp[ct-1][0]+2ll*dp[ct-1][1]+dp[ct-1][2]-(ct>1))*s0)%mod;
dp[ct][0]=(dp[ct][0]+1ll*(dp[ct-1][0]+dp[ct-1][1]+1+(ct>1))*((tp-1)%mod)+dp[ct-1][1])%mod;
dp[ct][1]=(1ll*dp[ct][1]+dp[ct-1][0]+dp[ct-1][2])%mod;
dp[ct][2]=(1ll*dp[ct-1][0]+dp[ct-1][1]+1+(ct>1))%mod;
}
return (as+3*ri+2+(ri>1))%mod;
}
int main()
{
scanf("%d",&T);
while(T--)scanf("%lld%lld%lld",&a,&b,&c),printf("%d\n",solve(b-a,c-b));
}
AGC056B Range Argmax
Problem
给定 ,有 个 的子区间,第 个区间为 。
对于一个 阶排列 ,使用如下方式构造一个长度为 的序列 :
对于一个 ,令 为 中最大值的下标,即 。
求可以得到的不同序列 的数量,模 。
Sol
考虑判断一个序列 是否可以被构造出。考虑当前所有被至少一个区间覆盖的位置中 最大的一个位置 ,可以发现所有覆盖了 的区间 都有 。接下来可以删去这些区间,继续这个过程直到所有区间被删完。
因此可以发现,一个序列 合法当且仅当可以使用如下方式删空所有区间:
找到一个位置 ,满足对于所有覆盖 的区间 ,都有 。然后删去这些区间。
但一个合法序列 可能有多个合法的删除方式,考虑取其中一种计数,这里选择每次删除当前所有被覆盖且可以选择的 中最小的一个。
考虑满足这样的条件的删除序列的性质。假设第一次删除的位置为 。删去 后,覆盖了 的区间都被删去了,接下来两部分 显然独立。因此一个必要条件是只保留 内部的区间及对应的 时存在删除方式,同时 也满足这一性质。
但这样不一定满足 是最小的能删除的位置。考虑使得这一性质得到满足的条件。可以发现右侧部分与 是否是最小无关,因此可以只考虑左侧部分。考虑找到只考虑 内部时,第一个可以删除的位置,记为 。设覆盖 的区间中左端点的最小值为 ,则如果 ,则不存在一个区间同时覆盖 ,可以发现在删去 之前 就可以删除,因此这样不满足性质。而如果 ,则 内所有当前可以选择的位置都被某个覆盖 的区间覆盖,因此选择 之前不能选择它们。所以这个条件得到满足,当且仅当 中第一个能被删除的位置大小 ,即 内使用上述方式得到的操作序列第一个选择的数 。
上述操作操作都是分成区间,因此考虑设 表示只考虑被 包含的区间,有多少个序列 满足存在合法删除方式且每次取最小的合法位置时,第一个删掉的位置大于等于 的序列数量。
记 表示只考虑被 包含的区间,这些区间中所有覆盖 的区间的左端点最小值。则由上述讨论,只考虑 包含的区间时,满足第一个合法位置为 的序列数量为:
对这个值求后缀和即可得到 ,注意区间内没有被覆盖的位置等边界情况。而 容易 求出,例如枚举 从大到小扫 。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 305
#define mod 998244353
int n,m,a,b,is[N][N];
int rb[N],ls[N][N][N],dp[N][N][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d%d",&a,&b),is[a][b]=1;
for(int r=1;r<=n;r++)
{
for(int l=1;l<=r;l++)if(is[l][r])rb[l]=r;
for(int l=r;l>=1;l--)
{
for(int i=1;i<=n;i++)ls[l][r][i]=ls[l+1][r][i];
for(int i=l;i<=rb[l];i++)ls[l][r][i]=l;
}
}
for(int d=0;d<=n;d++)
for(int l=1;l+d-1<=n;l++)
{
int r=l+d-1,fg=0;
for(int i=l;i<=r;i++)fg|=ls[l][r][i];
if(!fg){for(int i=l-1;i<=r+1;i++)dp[l][r][i]=1;continue;}
for(int i=l;i<=r;i++)if(ls[l][r][i])dp[l][r][i]=1ll*dp[l][i-1][ls[l][r][i]]*dp[i+1][r][i+1]%mod;
for(int i=r-1;i>=l;i--)dp[l][r][i]=(dp[l][r][i]+dp[l][r][i+1])%mod;
}
printf("%d\n",dp[1][n][1]);
}
AGC056D Subset Sum Game
Problem
有 个正整数 ,两个人进行博弈:
两人轮流操作,每个人每次可以拿走一个数,数拿完时游戏结束。
游戏结束时,第一个人获胜当且仅当他拿走的数总和在 之间。
求双方最优操作时,谁获胜。
Sol
AtCoder GuessTheLemma Contest
以下第一部分为乱搞思路
为了简便,以下看成有 个元素,且 为正整数。
考虑先手的一种策略:将这些数分成 个集合,可以有数不被分到集合中,通过操作使得每个集合中自己选一个数。如果先手要使得自己一定能达到目标,则除去第一个集合外,每个集合大小不能小于 。
如果这些集合满足从每个集合中任意选一个数先手都获胜,则显然先手必胜。可以发现一定会选 个大小为 的集合和一个大小为 的集合,因此可以得到如下结论:
不妨设 已经排好了序,如果存在 满足 ,且满足如下条件,则先手必胜:
设删去 后序列为 ,则 。
考虑结论的证明,上述条件这相当于先手选择了 ,然后将除去 外的元素按照大小关系两两配对,如果满足每一对中任意选一个先手都获胜,则先手必胜。而先手可以使用如下策略:
如果后手没有拿走 ,则拿走后手选择元素配对的元素,否则任意拿走一个元素,将它配对的元素作为新的 。
可以发现此时先手必胜。因此这是一个充分条件。
而通过提交一次可以发现这实际上也是必要条件。接下来给出证明:
如果只考虑第一个人操作时的获胜条件较为困难,因此同时考虑第二个人操作时第一个人的获胜条件,通过猜想可以得到如下结果:
第一个人操作时,他必胜当且仅当存在 使得如果删去 再将序列排序为 ,则 。
第二个人操作时(设此时元素个数为 ),第一个人获胜当且仅当存在 使得如果删去 再将序列排序为 ,则 。
这里认为每次第一个人拿走一个数后, 减去他拿走的数,这样最后第一个人获胜当且仅当 。
这在 时显然正确,考虑对 归纳,分别考虑两个问题:
- 由 (第一个人操作)时的必胜条件得到 (第二个人操作)时的必胜条件。
- 由 的必胜条件得到 的必胜条件。
考虑第一步,首先由 的必胜条件推出 的必胜条件。设当前元素为 以及 ,且它们使用这种顺序满足 的条件。
考虑沿用上面第一个人的策略。如果第二个人选择了一个 ,则第一个人可以选择与之配对的 ,然后钦定 不在接下来的配对中,这样就满足了 的必胜条件。如果第二个人选择 ,则第一个人可以任意选择,例如选择 并钦定 。因此如果满足 的必胜条件,则无论第二个人如何操作,得到的局面都满足 的必胜条件。
考虑不满足 的必胜条件的情况,即删去任意一个数后,排序得到的 一定使得 或者 。
设初始数排序后为 ,考虑删掉的数从 变化到 的过程。可以发现如果从 变化到 ,则 会增加,而 不变。而如果从 变化到 ,则 增加, 不变。
这意味着一次移动只改变一个数,但两个数中必须有一个满足条件。可以发现以下三种情况一定有一种得到满足:
- 第一个数一直满足条件,即任意情况下 。
- 第二个数一直满足条件,即任意情况下 。
- 存在一个时刻,两个数同时满足条件,即存在删去一个数的方式使得 。
证明:如果情况不满足前两个条件,则一个数一定在一次变化中由满足条件变为不满足条件或者反过来,因此这次变化前后另外一个数必须满足条件,从而有一个时刻两个数都满足条件。
如果满足第一种情况,则考虑删去第一个数,则 。此时后手可以直接每次拿走最大的数,先手拿的数不会超过 ,因此后手获胜。同理,满足第二种情况时,后手可以每次拿走最小的数获胜。
考虑第三种情况。考虑后手选择删去满足这个条件的数,需要证明先手不能获胜,相当于需要证明对于任意 ,如果 满足 ,则它不满足归纳的获胜条件。
只需要证明,在 的上述归纳条件中,无论如何选择 ,以下两条至少有一条成立:
考虑 的情况,枚举 的奇偶性:
- 如果 是奇数,则第一个不等式中,右侧会选择 ,而左侧会选择 。因此左侧小于右侧。
- 如果 是偶数,则第一个不等式中,右侧会选择 ,左侧会选择 ,同样左侧小于右侧。
同理可以证明,如果 ,则第二个不等式成立。
因此上述引理成立,从而可以证明,如果当前状态不满足 的条件,则后手可以删去一个数使得状态不满足 的条件。因此如果 时结论成立,则 时结论成立。
考虑第二步,如果先手存在一种满足条件的操作方式,则先手直接选择 ,接下来 一定满足 的条件。
而如果先手删去一个 后,某一个 满足 的条件,则可以发现这样的 满足 的条件。因此这一步是充分必要的。
这就完成了两步归纳过程,由归纳知结论成立。
考虑判断条件是否成立,枚举 ,预处理奇数位置的前缀和和偶数位置的前缀和即可。
这里还有一些性质,例如如果 是偶数,则将这个数 一定更优,如果 是奇数,则将这个数 一定更优。这一点可以通过一些讨论直接得到。因此只需要考虑较小的数是奇数,较大的数是偶数的情况,这样可以避免代码中的大量讨论。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 5050
#define ll long long
int n,v[N];
ll l,r,s[2][N],fg;
int main()
{
scanf("%d%lld%lld",&n,&l,&r);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
sort(v+1,v+n+1);
for(int i=1;i<=n;i++)
for(int j=0;j<2;j++)
s[j][i]=s[j][i-1]+((1^i^j)&1)*v[i];
for(int i=1;i<=n;i+=2)for(int j=i+1;j<=n;j+=2)
{
ll sl=s[1][i-1]+s[1][n]-s[1][j]+s[0][j-1]-s[0][i];
ll sr=s[0][i-1]+s[0][n]-s[0][j]+s[1][j-1]-s[1][i];
if(v[i]+sl>=l&&v[i]+sr<=r)fg=1;
if(v[j]+sl>=l&&v[j]+sr<=r)fg=1;
}
printf("%s\n",fg?"Alice":"Bob");
}
AGC056E Cheese
Problem
环上顺时针依次有位置 ,有 个人,第 个人在位置 。
接下来进行 次操作,每次操作随机在环上放置一个物品,对于每个 有 的概率将物品放置在 位置。
放置物品后,物品开始顺时针移动,物品经过一个人时,有 的概率人拿走这个物品并离开这个环,有 的概率不进行任何操作。
进行所有操作后,环上只会剩余一个人,对于每个 求第 个人留下的概率。
Sol
问题可以看成,一开始先将所有物品放到环上,然后让第一个物品开始移动直到被拿走,接下来选择第二个物品,以此类推。
称物品经过一个位置为一个事件,考虑交换相邻两个事件(如果它们不是同一个位置)。如果两个事件不在同一个位置则直接交换显然不影响。如果两个事件在同一个位置,但都没有改变这个位置的状态,则交换同样不影响。唯一的情况为两次事件中有一次当前位置的人拿走了物品,这种情况可以发现两个物品谁被拿走的两个情况是等价的。
因此这类似于可以交换事件的顺序而不改变答案,从而可以任意决定物品每一步移动间的顺序而不改变答案。因此有如下结论:
可以先将所有物品放到环上,然后按照位置从 到 将当前位置上的所有物品向右移动,然后重复这个过程。这样得到的答案和原答案相同。
一个我乱编的严格证明(不保证这个证明方式是对的):
考虑所有物品初始放置的位置确定的一种情况。
对于每个 ,考虑物品总共经过位置 的次数为 的所有情况。每个情况可以看成一列事件以及这些事件的结果(物品有没有被拿走)。考虑对这一列事件进行若干交换相邻事件的操作,使得事件顺序变为上面给出的后一种顺序。一种交换方式是在最前面归并,按照后一种顺序,依次将需要的位置向前交换到最前。
在交换两个事件时,如果两个事件不在同一个位置上或者两个事件结果相同则显然不影响概率。否则,如果原来之前的事件是当前人将物品拿走,则交换两个事件后这种情况的概率会乘 ,如果原来是之后的事件物品被拿走则交换后概率乘 。
但在这种情况下,考虑将之后两个事件对应的物品进行交换,这些事件之前的情况不影响这两个物品之后的事件,因此交换后也对应一种情况。考虑对所有情况一起交换,每次对所有情况选择一个位置同时向前,这样交换后对应的情况当前也交换到了对应状态。那么可以在会影响的状态间找到一个匹配,匹配一侧的情况的概率 ,另外一侧 ,匹配的两种状态相当于在这个位置的两个物品中,拿走一个留下另外一个,求之后的答案。那么如果能证明之后的情况和物品的编号顺序无关,即可得到这一步交换不影响答案。
而之后的情况物品数量一定减少,那么对 归纳一次即可。这样即可将一个 的所有情况交换到后一种顺序,且答案不变。
那么对于每个 ,经过 次数为 的情况对两种方式计算得到的答案相同,且经过 次 还有物品没有被拿走的概率随着 趋于 ,从而两个答案相同。
那么只需要对新的顺序算答案。此时物品间的顺序不重要,因此可以考虑每一段放的物品数,一种情况的贡献为 ,其中 为每一段放的物品数量。
考虑先处理第一轮的情况,这之后所有物品来到位置 。
对于之后的情况,设 表示当前有 个人, 个物品在位置 且一起顺时针移动,求最后第 个人留下的概率。
枚举向后第一个拿走物品的人即可转移,这里复杂度 。
这里只和剩下的人以及每个人的顺序有关。那么一个直接的想法是设 表示考虑了前 个位置,前面部分放了 个物品,有 个物品继续向后,这种情况的贡献和。再设 表示在这种情况下,第 个人留下来且在剩余人中在位置 的概率。这样转移需要枚举一段的物品数量,因此复杂度 ,但它确实能过。
更好的方式是注意到如果最后一个人在第一轮留下来,则他一定在最后。考虑计算一个人最后留下来的概率,将他看成最后一个人,钦定他在第一轮留下来,这样就只需要记录 ,但需要做 次。复杂度
Code
#include<cstdio>
using namespace std;
#define N 42
#define mod 998244353
int n,v[N],f[N][N],pr[N][N],c[N][N];
int sp[N][N][N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]),v[i]=1ll*pw(100,mod-2)*v[i]%mod;
f[1][1]=1;
for(int i=2;i<=n;i++)
{
int sp=pw(2,mod-i),tp=1;
for(int j=1;j<=i;j++,tp=1ll*tp*sp%mod)
for(int k=1;k<i;k++)f[i][(j+k-1)%i+1]=(f[i][(j+k-1)%i+1]+1ll*tp*(mod+1-sp)%mod*f[i-1][k])%mod;
int ir=pw(mod+1-tp,mod-2);
for(int j=1;j<=i;j++)f[i][j]=1ll*f[i][j]*ir%mod;
}
for(int i=1;i<=n;i++)
{
pr[i][0]=1;
for(int j=1;j<=n;j++)pr[i][j]=1ll*pr[i][j-1]*v[i]%mod;
}
for(int i=0;i<=n;i++)c[i][0]=c[i][i]=1;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
for(int s=1;s<=n;s++)
{
for(int i=0;i<=n;i++)for(int j=0;j<=i;j++)for(int k=0;k<=n;k++)sp[i][j][k]=0;
sp[0][0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=i;j++)
{
for(int k=n-1;k>=i-j;k--)for(int l=k+1;l<n;l++)
{
int tp=1ll*pr[(i+s)%n+1][l-k]*c[l][k]%mod;
sp[i][j][l]=(sp[i][j][l]+1ll*sp[i][j][k]*tp)%mod;
}
for(int k=n-1;k>=i-j;k--)
{
int ls=k-(i-j),pr=pw(2,mod-ls-1);
if(i+1<n)sp[i+1][j][k]=(sp[i+1][j][k]+1ll*sp[i][j][k]*(mod+1-pr))%mod;
sp[i+1][j+1][k]=(sp[i+1][j+1][k]+1ll*sp[i][j][k]*pr)%mod;
}
}
int as=0;
for(int j=1;j<=n;j++)as=(as+1ll*f[j][j]*sp[n][j][n-1])%mod;
printf("%d ",as);
}
}
AGC056F Degree Sequence in DFS Order
Problem
给定 ,考虑对一张有 个点, 条边,无自环但可以有重边的连通图从某一点开始进行 dfs(每个点记录是否访问,只访问一次),并依次记录访问到的每个点的度数。这样可以得到一个长度为 的序列 。
求有多少个长度为 的序列 能通过对一张满足上述条件的图进行上述过程得到。答案模 。
Sol
做题和写题解的时候有点神志不清,因此前半段过程有点意识流
将节点按照访问的顺序依次编号,考虑此时图的 dfs 树,dfs 树需要满足每个点的子树内点编号构成这个点向后的一段区间,且剩余边都是树上的返祖边。
可以发现树上前 个点一定连通,因此前 个点的度数和不小于 ,而 时还需要额外一条向后的边, 时有 ,因此一个条件是对于任意 ,都有 。
考虑对于一棵确定的 dfs 树,一个度数序列 是否可以被达到。此时相当于每个点有一个剩余度数,每次可以选择一对有祖先关系的点将它们的度数减一,求是否能让所有点的剩余度数都变为 。
此时可以得到一个类似 034E 的 dp;考虑一个子树内部进行匹配后,向上能留下的剩余匹配数。可以发现如果存在剩余匹配为 的方案,则一定存在剩余匹配为 直到子树内剩余度数之和的方案(考虑删一对匹配),因此只需要对于每个子树记录内部的最小匹配数 。考虑通过一个点所有儿子的 求出自己的 ,则子树内最小匹配数为儿子的 之和,最大匹配数为儿子的 之和(记 为子树内剩余度数之和)。记当前点剩余度数为 ,则最小匹配数为 ,可以发现 时, 会取到最小值 。 时会取 ,否则取 。最后存在方案当且仅当 。
可以发现,如果一个点的 大于它所有儿子的 之和,即子树内其它点的 之和,则当前点一定取最后一种转移,此时 一定确定。而其余点一定不取这种转移。那么从这些点往上,上面每个点的转移都是用 去减小子树 之和,直到最后减少为 。
考虑一个满足 大于它所有儿子的 之和的点 ,可以发现此时将子树内不是连向 的点改为连向 ,可以减小 增大另外一个 ,这样一定更优。因此只需要考虑原度数 大于子树内其他所有点的 之和的点 。
考虑一个 ,如果它满足 ,则这个点一定是上述情况。考虑第一个满足该条件的 (显然 满足条件,因此存在满足条件的点),则它的 至少为 (所有点连向 ),且这样一定最优。考虑 之前的情况,显然可能的最优方式是让所有剩余度数都连向 。可以发现这是能做到的:考虑连一条链,如果遇到 的点且不是第一个点,让它连到前面第一个 的点,然后将这个点的 减一继续这个过程,由之前的第一条限制这里一定能找到,剩余部分连出一条链,这样即可将所有额外度数连在一条链上。这样即可将所有剩余度数给 ,且可以发现这样的链可以保证之前的点都不满足 大于它所有儿子的 之和的点 。因此这里只需要满足对于第一个这样的 如下不等式成立:,由于 ,可以发现这个限制相当于 。
同时可以发现,如果对于第一个这样的 满足条件,则使用上述构造的 dfs 树满足条件。因此这是充分必要条件。
这里只要求了第一个满足条件的 ,但可以发现对于 的 ,它一定满足这个限制。对于不是第一个满足条件的 ,由前一个满足条件的 可以发现它一定也满足性质(),因此限制可以变为对于每个 都满足限制。因此序列 合法当且仅当它满足如下条件:
令 ,可以发现 满足如下几条限制:
- (由第一条限制可以得到 )
此时考虑容斥一组限制。可以发现如果容斥第一个限制,则第二个限制的形式难以优化,只能得到 的做法。
考虑容斥第二个限制,保留第一个限制。首先考虑没有第二个限制的情况。考虑序列的前缀和 ,相当于求有多少个单调不降的非负序列 满足 。
考虑将 看成一条如下折线:。可以发现这相当于一条从 到 ,只向上向右且不穿过 的折线。
且可以发现任意一条这样的折线都可以对于一组合法的 ,因此问题相当于求满足这个条件的折线数。考虑使用翻折方式去掉不合法的方案,即在不合法方案第一次经过 时翻折之后的路线,这样折线会到达 。因此合法方案数为 。
然后考虑容斥,即枚举若干个位置 ,钦定这些位置满足 。此时考虑第一个限制,可以发现如果一个位置 满足 ,且之前的 满足第一个限制,则 ,从而无论之后的 如何取,都一定满足限制。因此设第一个容斥的位置为 ,考虑将所有容斥位置的 减去 ,问题变为求有多少个序列 满足 且对于前 个位置 有 。
可以发现如果容斥后序列总和小于 ,则一定不存在合法方案。考虑一个 ,容斥 后总和减小 ,还剩余 。因此剩余的容斥最多可以让总和减去 。但容斥一个位置最少让总和减去 。因此可以发现只需要考虑容斥时枚举一个位置的情况。
因此只需要在不考虑第二个限制的问题答案上减去如下结果即可得到答案:
对于 ,求和满足如下限制的序列 的方案数:
此时限制不能穿过的部分变为 上的一条线段,因此不能直接使用翻折。
考虑枚举第一个穿过了 的位置以统计不合法方案数。设穿过的位置为 ,可以发现到达这个位置而之前不穿过的方案数为 。此时只需要考虑所有 的问题(这些问题在这里穿过会导致不合法),对于一个 ,穿过后的方案数显然为 ,对 求和即为这种情况的贡献。由 可以发现求和的结果为 。
这样就得到了所有不合法的方案数。结合三部分可以发现答案为:(三部分依次为容斥前的方案数,第一次容斥后的方案数,二次容斥的方案数)
直接计算即可,复杂度
Code
#include<cstdio>
using namespace std;
#define N 2000050
#define mod 998244353
int n,m,fr[N],ifr[N],as,c[N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int C(int x,int y)
{
if(x<y||y<0)return 0;
return 1ll*fr[x]*ifr[y]%mod*ifr[x-y]%mod;
}
int main()
{
scanf("%d%d",&n,&m);
fr[0]=1;for(int i=1;i<=m*2;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[m*2]=pw(fr[m*2],mod-2);for(int i=m*2;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
c[0]=1;for(int i=1;i<=n;i++)c[i]=1ll*c[i-1]*(4*i-2)%mod*pw(i+1,mod-2)%mod;
as=(C(m*2,n-1)-C(m*2,n-2)+mod)%mod;
for(int i=n;i>=1;i--)as=(as+mod-C(m+i-2,n-1))%mod;
for(int j=0;j<n-1;j++)
as=(as+1ll*c[j]*(C(m+n-j*2-2,n-j-1)-C(m-j-1,n-j-1))%mod)%mod;
printf("%d\n",as);
}
AGC055C Weird LIS
Problem
给出 ,求有多少个长度为 的正整数序列 满足如下性质:
- 存在一个 阶排列 ,使得 为排列中去掉 后的 LIS 长度。
答案对给定质数 取模。
Sol
显然删去一个位置最多使得 LIS 长度减少 ,且减少当且仅当这个位置在所有 LIS 中。
首先考虑 全部相同的情况。此时所有位置全部在所有 LIS 中出现或者都不在所有 LIS 中出现。
如果是第一种情况,则显然 LIS 长度为 ,因此 。如果是第二种情况,考虑 LIS 可能的长度,可以得到如下结论:
如果每个位置都不在所有 LIS 中出现,则 LIS 长度小于等于 且每个这样的 LIS 长度都可以取到。
证明:取 即可得到一种构造方式。接下来考虑充分性。
记 表示以 结尾的 LIS 长度,则如果存在一个 使得只有一个 满足 ,则显然所有 LIS 必须经过 ,矛盾。因此对于每个 至少有两个 使得 ,因此 LIS 长度不超过 。
这说明这种情况下需要满足 ,因此 全部相同时,满足第二个条件当且仅当 或者 。
考虑 不全部相同的情况,此时可以唯一确定每个位置是否在所有 LIS 中都出现,这种 满足条件当且仅当当前序列的 LIS 等于 。
考虑必须在 LIS 中出现的位置,它们将序列分成了若干段,最后的 LIS 相当于在每一段内选一些数,然后选上必须出现的位置。考虑一段内部,内部每个位置都不在所有的 LIS 中。一种方式是让这一段内部的值全部取在两侧必须在 LIS 中的位置的值中间,这样不同段之间一定不干扰。由上一个结论,如果这一段长度为 ,则这一段可以贡献 中的任意 LIS 长度。
但这样不能贡献 的 LIS 长度,贡献 的 LIS 长度只能让这一段所有元素都取其它值域,这样可能会互相影响。但可以发现,在限制 时,最终的 LIS 长度大于等于 。此时可以证明,如果钦定每一段的贡献为 中的一个 LIS 长度且最后的总 LIS 长度大于等于 ,则这样的钦定是合法的。
证明:如果一段钦定的贡献量不是 ,则使用之前的构造即可。否则,设这一段两侧的位置为 ,以这两个位置结尾的 LIS 长度为 。则如果 ,考虑将这一段所有位置设为最小值,这样如果一个上升子序列经过一个这样的位置 ,它们只能从 开始。而如果之后的位置满足不存在 之间的元素,则从 出发之后的 LIS 长度不超过从 出发之后的 LIS 长度,因此经过 的上升子序列都不是最长的,它不会造成影响。为了达到后一点,可以先决定哪些位置取最小值,然后将这些位置从后往前赋值 。
同样的,如果 ,其中 为总的 LIS 长度,则可以将这一段内所有位置设为最大值,类似讨论可以得到经过这些位置的上升子序列也没有影响。而在 时, 一定有一个得到满足,因此这一构造满足条件。
因此假设有 个必须在 LIS 中的位置,则 LIS 下界为 ( 时存在问题,但接下来只需要计算 的情况)。设这些位置下标为 ,额外令 ,则 LIS 上界为 。题目要求 LIS 长度在 间。
这可以看成一个长度为 的序列,先选择一些位置为在所有 LIS 中的位置,然后再放若干个长度是 的段,两两不交且不覆盖之前选的位置。最后的 LIS 长度为放的位置数加上段数。为了使得每个 LIS 长度在放长度为 的段的时候只被计算一次,考虑放一段时尽量向左放,这样只会被计算一次,而且是最优的放置方式。
而尽量向左放的条件还可以看成,如果 位置没有被选中且没有被段覆盖,且 位置没有被选中,则之后不能再放长度为 的段。因此问题可以看成如下形式:
求有多少种在长度为 的序列上放若干个长度为 且两两不交的段的方式满足如下限制:
- 至少放一个长度为 的段。
- 如果位置 没有被覆盖且位置 不被长度为 的段覆盖,则从 向后不能再放长度为 的段。
- 放的总段数在 间。
因此可以设 表示前 个位置放了 段,当前第 个位置是否被覆盖,是否满足了条件时的方案数,加入位置时枚举是否放 即可。这里可以不考虑第一个条件,不放长度为 的段的情况数容易计算,甚至正好相当于计算了第一类情况的一部分,处理一下即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5050
int n,k,p,dp[N][N][2][2],as;
int main()
{
scanf("%d%d%d",&n,&k,&p);
dp[0][0][1][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=i&&j<=k;j++)
{
for(int s=0;s<2;s++)for(int t=0;t<2;t++)dp[i+1][j+1][1][t]=(dp[i+1][j+1][1][t]+dp[i][j][s][t])%p;
for(int s=0;s<2;s++)dp[i+1][j][0][1]=(dp[i+1][j][0][1]+dp[i][j][s][1])%p;
dp[i+1][j][0][0]=(dp[i+1][j][0][0]+dp[i][j][1][0])%p;
dp[i+1][j+1][1][0]=(dp[i+1][j+1][1][0]+dp[i][j][0][0])%p;
dp[i+1][j][0][1]=(dp[i+1][j][0][1]+dp[i][j][0][0])%p;
}
as=(n>3)+(k==n-1);
for(int j=3;j<=k;j++)for(int s=0;s<2;s++)for(int t=0;t<2;t++)as=(as+dp[n][j][s][t])%p;
printf("%d\n",as);
}
AGC055D ABC Ultimatum
Problem
给定正整数 ,称一个长度为 的字符串是好的,当且仅当它可以被划分为 个子序列,每个子序列为 ABC
,BCA
,CAB
之一。
给一个长度为 ,包含 ABC?
的字符串,求有多少种替换所有 ?
的方式,使得得到的字符串是好的。答案对 取模。
Sol
考虑 AB
两种字符,除了第二种序列外,另外两种序列内 A
都在 B
之前。因此如果将 A
看作 ,B
看作 ,其它字符看作 ,考虑序列的最小前缀和的相反数,记作 ,则可以发现任意一种划分中,BCA
的数量不能小于 。
类似地,可以得到 。因此可以得到合法的一个必要条件,即如果合法一定有 。
通过一些方式可以发现这也是充分条件,即有如下结论成立:
一个字符串合法,当且仅当 。
一种直接但过于复杂的证明方式:
只需要证明存在合法的划分方式即可。考虑将前 个 A
,前 个 B
,前 个 C
作为开头,对于剩下的每个字符,从前面找一个当前没有使用过的字符作为它的前驱,且要求一个序列中相邻两个字符间满足 B
在 A
后,C
在 B
后,A
在 C
后。由 的定义,这样显然合法。
这样划分后可以得到 个序列,序列都可以看成 ABCABC...
的子串。如果每个序列长度都是 的倍数,则直接将每个子序列划分为若干长度为 的序列即可得到合法的方式。否则,考虑通过调整达到这一点。
考虑对于每个序列从后往前分长度是 的子序列,保留前面的部分,对这部分进行调整。首先考虑 AB
和 C
,如果它们的相对位置不是 ACB
,则可以直接进行拼接,拼接后进行一些调整,可以使得总序列数量不变。
如果是 ACB
,考虑分情况讨论:
- 如果
AB
后面接了一个长度是 的串,则ACBCAB
可以划分为A_BC__
加上CAB
。 - 如果
C
后面接了一个长度是 的串,则ACABCB
,ACABBC
,ACBABC
都可以划分为两个合法的序列。 - 否则,因为总序列数不超过 。一种情况是存在另外一个长度大于等于 的子序列作为某个子序列的末尾(且不是
CABC
,否则对这两个用上一种情况)。使用代码直接验证所有情况可以发现都能调整成对应的 或者 。最后给出了验证代码。
那么这样调整后长度 的段数量会减小,因此调整总会停止。
停止时,只可能同时剩下 A
,B
,C
或者同时剩下 AB
,BC
,CA
。
对于第一种情况,只考虑这些字符,一定存在子序列 AB
,BC
,CA
中的一个,配对后继续做即可。对于第二个可以类似的拼接出 ABCA
,BCAB
,CABC
。这样的过程会减少这些段的数量,因此最后一对可以调整到每个序列长度都是 的倍数。
std 证明方式:
考虑将当前字符串拼接三次,然后取第 个 A
,第 个 B
,第 个 C
。那么每种字符一定选出了 个中连续的 个,因此一个字符复制的三份中一定正好有一份被选。
由 的定义,显然第 个 A
在第 个 B
之前。同理可得第 个 C
在第 个 A
之前。因此这样选择的方式中,第 个 C
在第 个 A
之前。
因此选的三个位置距离不超过 ,因此对原序列循环位移一定可以使得三个位置按顺序出现,这说明这三个位置在原序列上是 ABC
的循环位移,因此这样的构造合法。
考虑记录当前前缀中每种字符出现的数量,可以发现一个 就是所有时刻两种出现数量的差的最大值。因此设 表示当前三种字符分别出现 次,前面三个最大值分别是 的方案数,直接转移即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 17
#define mod 998244353
int n,dp[N][N][N][N][N][N],as;
char s[N*3];
int main()
{
scanf("%d%s",&n,s+1);
dp[0][0][0][0][0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)for(int k=0;k<=n;k++)
for(int p=0;p<=i;p++)for(int q=0;q<=j;q++)for(int l=0;l<=k;l++)if(p+q+l<=n)
if(i+j+k==n*3)as=(as+dp[i][j][k][p][q][l])%mod;
else for(int vl=0;vl<3;vl++)if(s[i+j+k+1]=='?'||s[i+j+k+1]=='A'+vl)
{
int ni=i+(vl==0),nj=j+(vl==1),nk=k+(vl==2);
int np=max(p,ni-nk),nq=max(q,nj-ni),nl=max(l,nk-nj);
dp[ni][nj][nk][np][nq][nl]=(dp[ni][nj][nk][np][nq][nl]+dp[i][j][k][p][q][l])%mod;
}
printf("%d\n",as);
}
AGC055F Creative Splitting
Problem
给定正整数 。
定义一个长度为 的序列 是好的,当且仅当 。
考虑所有长度为 ,且满足如下性质的序列 :
- 是 间的正整数
- 可以被划分为 个好的序列
对于每一对 ,求出有多少满足性质的 使得 ,答案模给定质数。
Sol
首先考虑如何判定一个序列是否合法。这里有非常多假掉的贪心,例如如下尝试:
考虑顺序决定划分。一个形象的描述是将 个序列看成 行 列的网格,加入一个数 相当于将其放在某一行当前最后一个填的数后面,要求只能放在第 列及之后。由对称性可以认为每一行填的数都比上一行更多。
但这样仍然不能确定每一步放在哪最优:例如如果当前填的最多的行填到了第 个,最少的行填到了第 个且剩余行都填的多于 。如果当前数为 ,那么当下一个数是 时,当前只能填在最多的一行。但如果接下来所有数都大于 ,那么当前只能填在最少的一行。因此顺序做是不可行的。
10000
11000
1110x
但经过大量尝试可以发现,倒过来做是可行的:倒过来相当于每次选择当前长度(剩余数量)大于等于 的一行,然后在这一行删掉最后一个元素。由对称性这里仍然认为有多个相同行时只能删最上面一行,从而任意时刻每一行长度从上到下单调不降。可以发现如下结论:
每次操作时,选择能删的行中长度最小的一定最优。
证明:如果选择了更长的一行 ,考虑下一次选择本来应该选择行 的操作。首先这段时间 的长度一定小于等于 的长度,考虑交换两次选择的行,这样两次操作仍然能进行,中间对 的每一次操作时 的长度都比之前多了 ,因此可以进行操作。(这样可能不再满足每一行长度单调不降,但不影响存在方案)
最后一步也说明了正着做不可行:将 换为 则结论不再成立。
那么这样得到了一个贪心判定合法的方式:维护这样的一个网格,倒着考虑每一个数,对于一个 ,从当前长度大于等于 的第一行中删去一个数,合法当且仅当能操作完。
考虑计数问题。转移可以看成在每一行删的方案数是这一行长度减去上一行长度。
任意时刻剩余位置的边界形成一条向右向下的折线,这里也可以对折线边界考虑。如果能简单地求从初始状态到某条折线的方案数和从折线到结束状态的方案数,那么原问题是容易的:枚举插入第 个数之前的边界,再考虑加入当前数后的边界到结尾。但暴力的折线状态数大概是 ,因此不能直接做。
打表观察,可以发现如下结论:
考虑一个状态到结束状态的方案数。设当前还没被删的位置中,第 列的高度是 。那么方案数为
考虑从开始状态到一个状态的方案数,设已经删去的部分中,第 列删了 个。同时定义 表示删了 个的列数量。则方案数为
根本想不到的证明:考虑每一行长度 的 差分。记差分为 ,可以发现转移在差分上相当于选择一个位置 ,然后 减一 加一,方案数是 。
那么这可以看成每个位置有 个球,所有球可区分,每次选一个球移动到下一个位置!!1
这样问题就解决了:到结束状态的过程相当于将所有球移动到终点,那么每个球移动次数固定,不同球独立,从而就是 。从初始状态开始的过程相当于要求最后每个位置上有 个球,因此先决定每个球在哪个位置,这部分是 ,接下来就是之前的 。
回到原问题,考虑枚举 ,这样容易在之前的折线上求出操作 后的折线:在经过第 后第一次向下走时,删掉这一块就得到了操作后的折线。那么考虑从左上往右下走,状态设为 表示当前走到 行 列,当前已经统计的折线上部分格子数为 (用向下或向右都容易统计),当前连着向右走了 步(处理 ),当前是否在经过第 列后还没有向下(处理操作 )。转移显然,最后通过 即可得到答案。
对单个 做的复杂度为 ,总复杂度 但和n3k4一个速度
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 23
#define M 405
int n,m,mod,dp[N][N][N][M][2],as[M][N],fr[M],ifr[M];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d%d%d",&m,&n,&mod);
fr[0]=1;for(int i=1;i<=n*m;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[n*m]=pw(fr[n*m],mod-2);for(int i=n*m;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
for(int rv=1;rv<=n;rv++)
{
memset(dp,0,sizeof(dp));
dp[0][n][0][0][0]=1;
for(int i=0;i<=m;i++)
for(int j=n;j>=0;j--)
for(int d=0;d<=n;d++)
for(int s=0;s<=n*m;s++)if(dp[i][j][d][s][0]||dp[i][j][d][s][1])
{
if(i<m)dp[i+1][j][0][s+j][0]=(dp[i+1][j][0][s+j][0]+1ll*(i==0?1ll*fr[n]*ifr[n-j]%mod:1)*ifr[d]%mod*dp[i][j][d][s][0])%mod;
if(i<m&&j<rv)dp[i+1][j][0][s+j][0]=(dp[i+1][j][0][s+j][0]+1ll*(m-i)*(i==0?1ll*fr[n]*ifr[n-j]%mod:1)%mod*ifr[d]%mod*dp[i][j][d][s][1])%mod;
if(j)dp[i][j-1][d+(!!i)][s][j==rv]=(dp[i][j-1][d+(!!i)][s][j==rv]+1ll*ifr[i]*ifr[m-i]%mod*dp[i][j][d][s][0])%mod;
if(j)dp[i][j-1][d+(!!i)][s][1]=(dp[i][j-1][d+(!!i)][s][1]+1ll*ifr[i]*ifr[m-i]%mod*dp[i][j][d][s][1])%mod;
}
for(int i=0;i<n*m;i++)for(int j=0;j<=n;j++)as[i+1][rv]=(as[i+1][rv]+1ll*dp[m][0][j][i][0]*fr[i]%mod*fr[n*m-i-1]%mod*ifr[j])%mod;
}
for(int i=1;i<=n*m;i++,printf("\n"))for(int j=1;j<=n;j++)printf("%d ",as[n*m+1-i][n+1-j]);
}
AGC054D (ox)
Problem
给出一个包含 (ox)
的字符串,长度为 ,保证左右括号数量相同。
你可以交换字符串的相邻两个字符,可以操作任意次。你需要操作后序列满足如下性质:
如果将所有 o
替换为 ()
,x
替换为 )(
,则可以得到一个匹配的括号序列。
求最小的交换次数。
Sol
考虑括号序列的基础形式,即将 (
看成 ,)
看成 。可以发现在判断合法时,o
可以直接删去,x
需要要求所在位置前缀和大于等于 。
考虑一种简化的情况:初始时每个位置前缀和小于等于 。在操作后,所有位置的前缀和大于 。可以看成初始第 个左括号在第 个右括号右侧,需要交换使得第 个左括号在第 个右括号左侧。
首先假设只能移动括号。考虑最优方案的性质,如果存在一个位置前缀和大于等于 ,则考虑这个位置右侧第一个 )
和左侧第一个 (
,一定是初始时 )
在左侧,考虑取消这两个括号的最后一些操作,使得这个 )
在这个 (
左侧,此时这一段前缀和从 变为了 ,因此不影响合法性而更优(有一个位置变为了 ,但这个位置右侧为 (
,中间没有 x
)。
因此最优方案一定满足每个位置前缀和都是 或 。从括号位置角度,这相当于第 个 (
在第 个 )
右侧。
考虑在这个方案上倒退一些操作,使得第 个 (
的右侧正好是第 个 )
,可以发现之后不同括号对间的操作独立,因此操作一定可以看成先变成这样,再进行之后的操作。
首先考虑之后的情况,当前状态相当于有若干个 ()
,你可以将每一对括号向两侧进行一些扩展,使得每一个 x
都在一对括号内。最小化这里的步数相当于最大化留在括号外的位置数量,因此相当于如下问题:
有一个包含 ox
的序列,有若干个位置上有 ()
,你需要在每相邻两个 ()
间选择一个区间,使得区间内全部为 o
,两侧也可以选择区间,但左侧区间必须包含左端点,右侧类似。最大化选择的区间的总长。
这个问题可以 解决,设 表示考虑到第 个位置,当前还没有放区间/当前位置被区间覆盖/之前已经放过了区间时的最大长度即可。
到上一步可以发现,即使可以移动 ox
,将一个 x
移动进一对括号不如将括号移动过来,因此最优解中不会进行 ox
间的相互移动,即可以继续使用上述做法。
这个 说明之后的情况只和这些 ()
的位置有关。考虑前半段,首先考虑让前半段操作次数最少的方式,每一对括号(第 个 (
和第 个 )
)都需要变成 ()
,那么一部分代价为每一对括号当前位置中间的 ox
数量,再考虑不同 )(
间的顺序关系,可以发现另外一部分代价等于每一对括号中间 (
的数量总和。这是代价的一个下界,同时只需要第 次将第 个 (
向左移动到第 个 )
左侧即可达到下界。
考虑哪些操作能达到下界。设第 对括号最后成为 ()
的位置在 ,则如果 递增且 在第 对括号初始的位置之间,则沿用之前的方式即可得到一个最优操作方式。同时因为任意位置前缀和小于等于 ,因此设第 对括号初始位置为 ,则 分别递增。考虑直接让 在 中选择,这样可能得到不递增的方式,但这种方式一定可以通过交换 和区间的配对关系得到递增的方式。
第二部分代价只和这些 有关,考虑第一部分不是最优的操作方式,它只可能是某个 在区间外。但注意到将 往区间外移动一位需要 的代价,而再下一步中移动一位只需要 的代价,可以发现这样做显然不优,因此第一部分只会选择最优方式。
考虑每个区间内选一个数的问题。这里 分别递增。对于位置 ,设有 个区间的 ,有 个区间的 ,则考虑了前 个位置后,前面至少需要放 个数,最多放 个数。这是必要条件,但显然也是充分条件,因此可以变为考虑这个条件。
再结合上一个 ,可以设 表示前 个位置,当前放了 个 ()
,剩余一维与之前相同。注意 ()
是放到 ox
串相邻两个位置之间的,上面的限制也有各种边界情况,需要仔细考虑。转移先枚举 之间怎么放,然后枚举下一个位置的情况。复杂度
最后考虑原问题,此时有一些段原先的前缀和就大于 。显然这些段内部括号不用操作,显然也不会将一个括号从这一段一侧移动到另外一侧(不如从这一段内拿一个出去操作),因此可以看成这些前缀和大于 的段将序列分成若干部分,每部分独立 。但可以在第二部分中,将这一段左侧第一个 (
向左包含一些 x
,或者将右侧第一个 )
向右。可以发现这相当于在上面的 中,如果当前这一段右侧是一个 (...)
的段,则 时选择的最后一个区间可以不包含最右侧端点。在上一种情况上进行一些处理即可,注意细节。
复杂度
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
#define N 4050
int n,si[N*2],dp[N][3],v1;
char s[N*2];
queue<int> tp;
int main()
{
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)si[i]=si[i-1]+(s[i]=='(')-(s[i]==')');
for(int i=1;i<=n;i++)if(si[i]<0||si[i-1]<0)
if(s[i]==')')tp.push(i);
else if(s[i]=='(')
{
int u=tp.front();tp.pop();
for(int j=u;j<=i;j++)if(s[j]!='(')v1++;
}
for(int i=0;i<=n/2;i++)for(int j=0;j<3;j++)dp[i][j]=1e9;
dp[0][1]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<n/2;j++)for(int k=0;k<3;k++)dp[j+1][0]=min(dp[j+1][0],dp[j][k]);
for(int j=0;j<=n/2;j++)for(int k=0;k<2;k++)dp[j][k+1]=min(dp[j][k+1],dp[j][k]);
int tp=si[i]<=0;
if(s[i]=='o')for(int j=0;j<=n/2;j++)for(int k=0;k<3;k++)dp[j][k]+=k!=1&&tp;
if(s[i]=='x')for(int j=0;j<=n/2;j++)dp[j][1]=1e9,dp[j][0]+=tp,dp[j][2]+=tp;
if(si[i]>si[i-1]&&si[i]<=0)for(int j=0;j<=n/2;j++)for(int k=0;k<3;k++)dp[j][k]=dp[j+1][k];
int li=si[i]<0?-si[i]:0;
for(int j=li+1;j<=n/2;j++)for(int k=0;k<3;k++)dp[j][k]=1e9;
if(si[i]>0)dp[0][0]=min(dp[0][0],min(dp[0][1],dp[0][2]));
}
v1+=min(dp[0][0],dp[0][1]);
printf("%d\n",v1);
}
AGC054E ZigZag Break
Problem
称一个排列 是好的,当且仅当可以使用如下操作若干次使得 只剩下两个元素:
任意选择当前连续的三个元素 ,满足 或者 ,接下来删去 。
次询问,每次给定 ,求有多少个好的 阶排列以 开头。
Sol
最后只会留下开头和结尾。考虑开头结尾的数 ,由对称性不妨设 。
考虑将 的数看作 , 的数看作 ,剩余的数看作 。可以发现只需要将所有 删掉,接下来每次选择最大数或最小数就可以达到目标,因此考虑如何删掉 。
考虑操作的一些性质,如果有两个 ,考虑选择这两个 ,然后在这一段内一直删最大值,这样一定可以删完这一段的 ,留下若干个 。对于两个 也可以类似操作。
注意到序列开头为 ,结尾为 ,结合上一个操作可以发现如果序列中存在连续的 ,则可以选择这个 和开头操作一次,选择 和结尾操作一次,这样就达成了目标。
否则,如果不存在连续的 ,此时最后需要留下 ,而如果一次操作删去了一个数使得出现了 ,则这次操作一定形如 然后删去 ,但这样 不能是 ,因此删去前也存在 。这就说明如果初始不存在连续的 ,则不可能通过操作得到连续的 。
因此一个排列合法当且仅当对应的序列存在 作为连续子序列。这也说明除去开头结尾外,同一种数内部大小关系可以任意排列。
考虑枚举 ,此时相当于有 个 , 个 , 个 ,求有多少个排成的序列满足 在开头, 在结尾且存在 。
考虑算不合法的方案数,那么可以将整个过程分成如下两步:
- 先放入所有 ,然后放入 ,使得第一个位置为 且最后一个位置为 (否则不能放 )
- 接下来在紧跟在 后面的位置放入 ,使得最后一个位置为 。可以发现这相当于无视之前的 ,只考虑 的位置关系,使得第一个位置为 最后一个位置为 。
因此不合法的方案数为 。
如果对 求和,则可以发现相当于如下结果(令 ):
于是可以 计算这部分,对于 的情况相当于将 变为 再做一次求和。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 1000500
#define mod 998244353
int T,n,a,fr[N],ifr[N];
int solve(int n,int a)
{
int as=n-a+1;
if(a+1<=n)as=(as+1ll*a*fr[a]%mod*ifr[a+1])%mod,as=(as+mod-1ll*(n+1)*fr[a+1]%mod*ifr[a+2]%mod)%mod;
return 1ll*fr[n]*as%mod;
}
int main()
{
fr[0]=1;for(int i=1;i<=1e6;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[1000000]=490058372;for(int i=1e6;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
scanf("%d",&T);
while(T--)scanf("%d%d",&n,&a),printf("%d\n",(solve(n-2,a-1)+solve(n-2,n-a))%mod);
}
AGC053C Random Card Game
Problem
有 张卡牌,第 张卡牌上有数字 。现在将 张牌随机分成两列,每列长度都是 。接下来进行如下游戏:
在游戏中,你需要重复进行如下操作,直到有一列牌被删空:
选择一个 满足当前两列牌的数量都不小于 ,考虑当前两列牌分别从左向右的第 张牌,删去这两张牌中数字更小的一张。
你希望最小化操作次数,你可以看到当前每张牌上的数字。
给定 ,求随机分成两列后最小操作次数的期望,答案模 。
Sol
考虑如何确定一种情况的最小步数。显然 所在的一列不能被删掉,只能删空另外一列,相当于让 所在的一列删掉的尽量少。设 所在的列从左向右为 ,另外一列为 。
第二列需要被删空,考虑每个位置被谁删掉。记第二列第 个位置被第一列第 个位置删掉,为了能被删掉,显然需要有 。
考虑第一列至少需要删多少次,由于删掉这个元素时需要让第二列第 个位置和第一列第 个位置当前在同一位置上,因此此时第一列删除的元素数量至少为 。那么第一列至少需要删除 个元素。
先考虑如何最小化这个下界,显然可以让每个 都取最小的满足 的 。可以发现,此时可以通过操作达到这个下界。这里给出一种构造方式:
如果当前所有 ,则考虑选出最大的 使得 ,然后删去第二堆中的 ,这之后所有位置仍然满足 (使用新的编号后),重复这个过程即可,这样不需要删去第一列的元素。
否则,设 ,考虑找到第一个满足 的位置,由于之前的位置都有 ,且显然上述方式找的的 单调不降,因此没有一个位置的 等于 。而由上述找 的方式,可以发现 ,因此可以删去第一列的位置 。由于找的是第一个位置,因此删去之后 一定减少一,这样删 次就变为了上一种情况。
因此可以通过 算出答案。接下来考虑期望线性性,即对于每个 计算第一列至少需要删 次的概率,对所有正整数 求和概率,再加上第二列的 次即可得到答案。
考虑计算删的次数不超过 的概率,这相当于满足 的概率,相当于求对于任意 都满足 中的最大值大于 的概率。
那么考虑按照 的顺序加入所有数,可以发现条件变为加入一个 时,它不能是当前加入的所有元素中最大的一个。可以发现这样的概率为:
预处理阶乘和阶乘逆元,以及双阶乘()和它的逆元,即可 求一个概率。然后容易由线性性得到答案。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 2000600
#define mod 1000000007
int n,fr[N],ir[N],sr[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d",&n);
fr[0]=1;for(int i=1;i<=n*2;i++)fr[i]=1ll*fr[i-1]*i%mod;
ir[n*2]=pw(fr[n*2],mod-2);for(int i=n*2;i>=1;i--)ir[i-1]=1ll*ir[i]*i%mod;
for(int i=1;i<=n*2;i++)ir[i]=1ll*ir[i]*fr[i-1]%mod;
int ifn=ir[n];
for(int i=2;i<=n*2;i++)ir[i]=1ll*ir[i]*ir[i-2]%mod;
sr[0]=sr[1]=1;for(int i=2;i<=n*2;i++)sr[i]=1ll*sr[i-2]*i%mod;
for(int i=1;i<=n;i++)
as=(as+1ll*ir[n*2+1-i]*sr[i-1]%mod*sr[n*2-i]%mod*ir[i]%mod*i%mod*(n*2+1-i)%mod*ifn)%mod;
printf("%d\n",(n*2+mod-as)%mod);
}
AGC053D Everyone is a Winner
Problem
有 个人,他们都做了 道题目。
对于第 个人,他有 道题目用时 分钟, 道题目用时 分钟, 道题目用时 分钟。
每个人可以任意选择做题的顺序,求是否存在一组顺序满足如下条件:
对于每个 ,第 个人最先做出 道题。即设 表示第 个人做出前 个题需要的总时间,则有
多组数据
Sol
显然将 换为 不影响问题的形式。
设第 个人完成前 道题的时间为 ,则一组合法方案需要满足对于任意一个人,他完成前 道题的时间大于等于 。决定做题的顺序相当于将这个人的所有 排成一列,这一列的前缀和为做完 道题的时间,限制可以看成对前缀和的限制。
考虑相邻两个人的情况,可以发现 加上第 个人的第 个数必须 , 减去第 个人的第 个数必须 ,可以得到 ,即 。
考虑一个人的情况,他的序列需要满足前 个数和为 ,其余前缀和不能小于对应 。考虑当前位置的限制,即必须能选出 个数使得和为 。因此 不能大于当前人有的数中前 大的数之和,不能小于前 小的数之和。且如果 ,则 必须是偶数。可以发现只要满足这些条件,就一定可以选出数使得和为 。
考虑其它位置的限制,如果确定了前 个数中 的数量,则在 内显然可以通过按照 的顺序放做到最大化其余所有前缀和。这样在相当于在两个区间内分别给出了一个斜率分段为 的上凸壳,要求剩余 都在上凸壳下方。注意到左侧区间的上凸壳开始时的坐标为 ,结束时为 ,而 ,因此左侧斜率为 的两段不会造成影响,只需要考虑中间一段,即左侧的 不能大于等于某个值。再考虑右侧段,如果当前凸壳结束时的坐标大于等于 ,则类似地可以得到右侧只需要考虑中间一段,而如果不满足这个条件,则相当于第 个人完成 个题的时间小于最后一个人完成 个题的时间,此时显然无解。
除去上一种无解情况后,可以发现如果确定了前 个数中 的数量,则限制相当于要求编号小于等于 的 不能超过某个值,编号大于等于 的 不能超过某个值。考虑这部分的性质。设左侧的最大值为 ,右侧的最大值为 。考虑总的 的数量,可以得到 ,考虑 的数量可以得到 。
此时可以发现,如果 满足上述两条限制以及 的限制,则可以对于第 个人构造一种序列满足前缀和的限制。构造方式可以考虑先贪心放左侧,用尽量少的 ,然后放右侧,可以发现右侧只要满足对应限制就可以构造出来,这里省略细节。
这里的限制可以看成若干个 ,其中右侧为 中的一些相加,再加上一些常数得到的, 也可以表示为这种限制。因此考虑初始让 全部取到下界,然后选择一个不满足条件的 增加。如果能在不超出上界的情况下满足所有要求则有解,否则显然无解。
但显然不能直接维护这个过程,考虑这个过程的性质。设下界最大的位置为 。则对于剩下的每一个位置,如果它被增加到了 ,则此时它一定满足限制,且在有数大于了 之前它一直满足限制。但有一些数有奇偶性限制,它们可能从 增加到 ,但可以发现这些位置满足无论其它位置怎么取,关于这个位置的 条件一定得到满足,因此这个位置取 一定满足所有限制(上面两个限制以及 )。
因此这个位置一定是全局最大值,且它的值不会改变。考虑它右侧的情况,此时每个位置的 固定,相当于给每个位置了一个新的下界,同时要求 。如果没有 的限制,则可以从右往左贪心放最小的值。在加入这个限制后,考虑先在从后往前填时要求 ,这样只有可能出现 的情况,此时必须增加 ,可以发现需要从 位置向后依次增加满足相邻位置的限制。但注意到这样的增加一定不影响关于 的两条限制( 不变, 增加一个值, 最多增加对应值),因此只需要增加后不超过上界就是合法的。此时一种做法是两个方向各自扫一次,也可以对上界下界同时扫一次。这里需要注意奇偶性的情况,但可以发现不会影响上面的过程。
考虑左侧的情况,此时限制变为 ,与上面类似地处理即可。
复杂度
我不知道为啥我之前那个从左往右一次再从右往左一次是对的。
我还不知道为啥题解那个直接从右往左一次,取最大 是对的。
Code
#include<cstdio>
using namespace std;
#define N 200500
int T,n,s[N][3],lb[N],rb[N];
int calc(int a,int b,int c,int s)
{
int as=0;
if(a>=s)return s;else as+=a,s-=a;
if(b>=s)return as;else return a+b-s;
}
int solve()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)for(int j=0;j<3;j++)scanf("%d",&s[i][j]);
for(int i=1;i<=n;i++)lb[i]=-calc(s[i][0],s[i][1],s[i][2],i),rb[i]=calc(s[i][2],s[i][1],s[i][0],i);
for(int i=1;i<=n;i++)if(s[i][2]-s[i][0]<s[n][2]-s[n][0])return 0;
int fr=0;for(int i=1;i<=n;i++)if(lb[i]>=lb[fr])fr=i;
int ls=0;
for(int i=1;i<=fr;i++)
{
if(lb[i]<ls+lb[fr]-s[i][2])lb[i]=ls+lb[fr]-s[i][2];
if(lb[i]<ls-s[i][0])lb[i]=ls-s[i][0];
if(!s[i][1]&&(i+lb[i])%2)lb[i]++;
if(ls<lb[i])ls=lb[i];
}
for(int i=fr-1;i>=1;i--)
{
if(lb[i]<lb[i+1]-1)lb[i]=lb[i+1]-1;
if(!s[i][1]&&(i+lb[i])%2)lb[i]++;
}
ls=lb[n];
for(int i=n-1;i>=fr;i--)
{
if(lb[i]<lb[fr]-s[i][0])lb[i]=lb[fr]-s[i][0];
if(lb[i]<ls+lb[fr]-s[i][2])lb[i]=ls+lb[fr]-s[i][2];
if(ls<lb[i])ls=lb[i];
}
for(int i=fr+1;i<=n;i++)
{
if(lb[i]<lb[i-1]-1)lb[i]=lb[i-1]-1;
if(!s[i][1]&&(i+lb[i])%2)lb[i]++;
}
for(int i=1;i<=n;i++)if(lb[fr]>s[i][2]||lb[i]>rb[i])return 0;
return 1;
}
int main()
{
scanf("%d",&T);
while(T--)printf("%s\n",solve()?"Yes":"No");
}
AGC053E More Peaks More Fun
Problem
有 个二元组 ,保证 中的每个数在这些二元组中正好出现一次。
你需要将这些二元组排成一列,求 种方式中有多少种排列方式满足如下条件:
你需要对于每个二元组选择是否交换二元组中的两个数,交换后将所有二元组按顺序排成一列,得到排列 。存在一种方式使得排列满足如下性质:
设排列为 ,存在 个 满足 。
答案模
Sol
考虑给二元组内部选择顺序的问题。不妨设 ,如果最后选择了 的顺序,则称这一段为 /
,否则称这一段为 \
。
考虑排成一列后,哪些位置满足条件。每一对两个数间最多有一个满足条件,这样最多有 个。还可以发现,如果第一段是 \
,则会减少一个满足条件的位置(第一个一定不满足条件),如果最后一段是 /
也会减少一个。同时如果存在相邻两个二元组为 /\
,则中间两个位置中最多有一个满足条件,这样也会减少一个满足条件的位置。
因此可以发现,最多有 个位置满足条件,且达到这个要求必须使得所有二元组的顺序形如 //.../\\...\
,其中 /
的一段可以为空,\
的一段也可以为空。设左侧位置为 ,此时需要满足除去中间两个位置外,两侧的每一个 /
和 \
都贡献了一个满足条件的位置。即如下两条:
因此一个二元组的排列合法,当且仅当存在一个 满足如上限制。
考虑找到最大的一个 满足 ,为了满足第二个条件,上面的 不能取小于 的数。注意到第一个条件是 越小越好,因此可以得到合法当且仅当取 满足条件,即二元组序列满足如下条件之一:
- ,使得 ,且 ,且
考虑第一个问题,相当于求有 个点,每个点对应一个线段 ,一个点 向另外一个点 有有向边当且仅当 ,即第二个线段不在第一个线段完全右侧。求这个图的哈密顿链的数量。接下来是魔法环节
通过一些尝试,可以发现如下结论:
对于每个二元组 ,看成在 位置放一个左括号,在 位置放一个右括号,这样得到一个长度为 的括号序列。
然后将 (
看成 ,将 )
看成 ,对于每一个 )
,统计它左侧所有数的和,乘积即为上述方案数。
证明:从括号序列的角度看,这可以看成两侧有这 个线段各一份,然后对左侧每一个线段 ,匹配右侧的一个线段 ,使得 ,匹配的方案数即为这个值。这也相当于有多少个排列 满足对于所有 ,线段 向线段 有边。
这也相当于找到若干个环,将每个点正好覆盖一次的方案数。考虑这样找到的若干个环,可以通过如下方式通过这些环得到一条链:
对于每个环,找到环中 最小的线段,从这里开始破环成链。然后将所有链按照开始线段的 从大到小排序拼接。
由于原先是环,因此每条链结尾线段的 都大于等于开头线段的 ,而下一条链开头的 更小,因此相邻链之间一定可以拼接。
同时可以通过如下方式由链得到若干个环:
每次找到链的开头,从开头一直向后直到下一个线段的 小于开头线段的 ,然后将前面这部分取出来拼接成一个环。
每次取出的部分中开头的 最小,因此显然可以拼接回一个环。
这样就得到了一个双射,因此若干个环覆盖所有点的方案数等于哈密顿链的方案数。
考虑第二部分,如果确定了中间位置的两条线段,则问题可以看成求给定两个开头,从这两个开头开始分别选一条链使得两条链覆盖所有点的方案数。
考虑将这两条线段的 变为 ,然后将两条链拼接成一个环,此时一定得到一个使用上述方式构造的图上的哈密顿回路。对于任意一个哈密顿回路,从这两个位置断开就得到了两条链。
那么首先需要得到如何求哈密顿回路的数量。同样通过一些尝试可以得到如下结论:
使用同样的方式构造括号序列,将 (
看成 ,将 )
看成 。
对于除去最后一个外的每一个 )
,统计它左侧所有数的和,再将其减一,乘积即为上述方案数。
同样考虑组合意义,可以发现这相当于 最小的一个线段不做为匹配右端, 最大的一个线段不做为匹配左端,求剩余位置匹配的方案数。
那么从 最小的线段到 最大的线段会有一条链,剩余的是一些环。
那么对这些环,使用之前的方式拼接出一条链,然后可以发现一定可以将这条链和上一条链拼接得到一个环(最小 和最大 一定可以做匹配),这样就得到了哈密顿回路。
而对于一个哈密顿回路,找到 最小的线段和 最大的线段,将这一段从环上拆下来,剩余部分使用之前的构造变回环即可。这样又得到了双射。
因此这部分可以看成如下问题:
当前每个右括号有一个权值 ,你需要选择两个二元组 ,满足 ,接下来给所有位置小于等于 的右括号权值加一,给所有位置小于等于 的右括号权值加一,此时贡献为所有右括号权值分别减一后的乘积。求所有情况的贡献和。
这可以看成给 的位置加一,给 的位置减一。这样两侧的贡献就独立了。
预处理如果选择一个二元组做为 时,左侧加一后会使得乘积乘上多少。然后枚举一个二元组作为 ,可以得到右侧减一对乘积的影响,此时需要求所有 的二元组作为 的贡献和,对上一步的贡献前缀和即可求出。
求一个二元组的贡献可以通过一些前后缀乘积得到。这里需要注意减一之后值可能为 ,因此选择 的情况有一些细节,但前缀加一不会出现 ,因此前缀部分可以直接求逆元得到乘积会乘上多少。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 405000
#define mod 1000000007
int n,a,b,ls[N],rs[N],vl[N],s1[N],ir[N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)ir[i]=pw(i,mod-2);
for(int i=1;i<=n;i++)scanf("%d%d",&a,&b),rs[a<=b?a:b]=a>b?a:b;
for(int i=1;i<=n*2;i++)ls[rs[i]]=i;
for(int i=1;i<=n*2;i++)vl[i]=vl[i-1]+(rs[i]?1:-1);
int tp1=1;
for(int i=n*2;i>=1;i--)
{
if(rs[i])tp1=1ll*tp1*(1+ir[vl[i]])%mod;
else s1[ls[i]]=(s1[ls[i]]+tp1)%mod;
s1[i]=(s1[i]+s1[i+1])%mod;
}
int tp=1,as=1;
for(int i=2;i<=n*2;i++)
if(rs[i])tp=1ll*tp*(mod+1-ir[vl[i]])%mod;
else as=(as+1ll*tp*s1[i])%mod;
for(int i=1;i<=n*2;i++)if(rs[i])as=1ll*as*vl[i]%mod;
printf("%d\n",as);
}
AGC052C Nondivisible Prefix Sums
Problem
给定质数 ,称一个长度为 的序列 是好的,当且仅当可以将 重排,使得 的每一个非空前缀和都不是 的倍数。
给定序列长度 ,现在要求序列的每个元素都在 之间,求好的序列的数量,模 。
Sol
考虑对于一个序列判断是否合法,即是否可以重排使得每个前缀和都不是 的倍数。
显的一个必要条件为序列所有数之和不为 的倍数,考虑其余条件。
考虑贪心构造:每次选择当前剩余数量最多的一种数,如果能放就放,否则考虑放当前剩余数量第二多的数,接下来一定可以接一个剩余数量最多的数。
由这个贪心,可以发现如下结论:如果序列 满足所有数之和不为 的倍数,且没有一种数出现次数大于 ,则它一定合法。
证明:考虑使用上述贪心方式构造,可以发现除去剩余一个数的情况外,每一步之后都满足当前没有一种数出现次数大于一半。而如果出现不能放的情况,此时一定只剩下一种数,而在贪心构造中只有最后一个数时可能出现这种情况,但条件保证总和不是 的倍数,因此这一构造不会出现这种情况。
考虑剩余情况,此时一定有一种数 出现次数大于一半。由于 是质数,考虑将所有数乘上 ,这样显然不改变合法性,而乘 后变为 的出现次数大于一半。
将所有数乘一个与 互质的数不改变合法性,因此可以得到所有 出现大于一半的序列和所有 出现大于一半的序列一一对应,这两部分中不合法的序列也一一对应。因此满足所有数之和不是 的倍数但不合法的方案数为满足所有数之和不是 的倍数, 出现次数大于一半且不合法的方案数的 倍。
对于 出现次数大于一半的情况,显然需要尽量多用 。
设除去 外的所有数为 ,考虑合法序列中 个数的最大值。如果贪心地能放 就放 ,则可以发现 的数量不超过 。如果 的数量不超过这个值,则使用贪心方式放,直到 出现次数不大于一半,由于上述上界是贪心得到的,因此这样一定可以构造出来。另一方面,如果 的数量大于这个值,则所有数总和大于 ,但对于每一个 ,都需要一个大于 的数使得前缀和跳过 ,因此至少需要 个这样的数,因此超过上界的情况一定无解。
则不合法的条件相当于 ,即 ,这相当于选 有 的代价,求有多少种长度为 的序列满足每个位置代价总和不超过 。但还有一个条件是所有数的总和不是 的倍数,可以发现这里 的代价 ,因此总和是 的倍数当且仅当代价总和 。
因此只需要记录代价总和就可以判定所有条件,因此记 表示选了 个数代价总和为 的方案数,使用前缀和优化转移即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5050
#define mod 998244353
int n,k,dp[N],su[N],as;
int main()
{
scanf("%d%d",&n,&k);
dp[0]=1;
for(int i=1;i<=n;i++)
{
su[0]=dp[0];
for(int j=1;j<=n;j++)su[j]=(su[j-1]+dp[j])%mod;
for(int j=0;j<=n;j++)
{
if(j>1)dp[j]=(dp[j]+su[j-2])%mod;
if(j>=k)dp[j]=(dp[j]+mod-su[j-k])%mod;
}
}
int f0=1,f1=0;
for(int i=1;i<=n;i++)
{
int v0=1ll*f1*(k-1)%mod,v1=(1ll*f1*(k-2)+f0)%mod;
f0=v0,f1=v1;
}
as=1ll*f1*(k-1)%mod;
for(int i=0;i<=n-k;i++)if((n-i)%k)as=(as+mod-1ll*(k-1)*dp[i]%mod)%mod;
printf("%d\n",as);
}
AGC052D Equal LIS
Problem
给定一个 阶排列 ,求能否将 划分为两个子序列,使得两个子序列的 LIS 长度相等。
多组数据
Sol
考虑原序列的 LIS,LIS 的子序列也是上升子序列,因此可以发现划分出的两个子序列的 LIS 长度之和不会小于原序列的 LIS 长度。
考虑原序列 LIS 长度为偶数的情况,设长度为 。由上一条考虑能否得到两个 LIS 长度为 的子序列。
可以发现这一定能做到,例如可以使用如下构造方式:
设 表示以 结尾的 LIS 长度,取所有 的位置作为第一个子序列,剩余位置作为第二个子序列。这样第一个子序列的 LIS 显然等于 ,由 LIS 的 dp 过程可以发现第二个子序列的 LIS 等于 ,这样就达到了目标。
更进一步,考虑对于一个集合 ,只保留所有 的位置。显然 相同的位置间,值随着位置增加而减少(否则 不会相同),因此这些位置的 LIS 不超过 ,而考虑原 LIS 留下的元素,可以发现此时 LIS 长度即为 。
考虑 LIS 长度为奇数的情况,设长度为 。此时至少需要有一个序列的 LIS 包含一个不属于原来某一个 LIS 的元素。因此至少需要有一个不属于某一个 LIS 的元素满足经过它的上升子序列长度不小于 。
考虑经过它的一个上升子序列 ,其中 不在某个 LIS 中,考虑如下构造:
对于 ,将所有 的 放入第一个子序列,将 放入第一个子序列。剩余元素放入第二个子序列。
则使用类似于上面的分析,第一个子序列中考虑 ,可以得到 LIS 长度为 ,第二个子序列中考虑原 LIS 也可以得到长度为 ,因此这种情况下合法只需要有一个元素满足条件即可。那么记录以每个元素开头/结尾的 LIS 长度,同时求出一个 LIS 的下标即可判断。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 205000
int T,n,p[N],f[N],g[N];
struct BIT{
int tr[N];
void add(int x,int k){for(int i=x;i<=n;i+=i&-i)if(tr[i]<k)tr[i]=k;}
int que(int x){int as=0;for(int i=x;i;i-=i&-i)if(tr[i]>as)as=tr[i];return as;}
}tr;
void doit(int *f)
{
for(int i=1;i<=n;i++)tr.tr[i]=0;
for(int i=1;i<=n;i++)f[i]=1+tr.que(p[i]),tr.add(p[i],f[i]);
}
void solve()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&p[n+1-i]),p[n+1-i]=n+1-p[n+1-i];
doit(g);
for(int i=1;i*2<=n;i++)g[i]^=g[n+1-i]^=g[i]^=g[n+1-i];
int li=0;
for(int i=1;i<=n;i++)if(li<g[i])li=g[i];
if(li%2==0){printf("Yes\n");return;}
for(int i=1;i*2<=n;i++)p[i]^=p[n+1-i]^=p[i]^=p[n+1-i];
for(int i=1;i<=n;i++)p[i]=n+1-p[i];
doit(f);
int nw=0;
for(int i=1;i<=n;i++)
if(p[i]>p[nw]&&f[i]==f[nw]+1&&f[i]+g[i]==li+1)nw=i;
else if(f[i]+g[i]>=li/2+2){printf("Yes\n");return;}
printf("No\n");
}
int main()
{
scanf("%d",&T);
while(T--)solve();
}
AGC052E 3 Letters
Problem
称一个只包含 ABC
的串是好的,当且仅当它不存在两个相邻字符相同。
给定长度为 的两个好串 ,你需要将 变为 ,你可以进行如下操作:
改变 某个位置的字符,要求改变后 仍然是好的。
求最小操作次数。
Sol
考虑能进行的操作,如果只看中间的操作,则可以发现只能选择一个两侧字符相同的数进行改变,例如在 ABA
,ACA
间改变。
那么考虑序列 的差分,可以发现好的序列满足差分都是 ,且上述操作相当于交换相邻的一组 。
然后考虑开头结尾的操作,可以发现操作开头相当于改变开头位置的差分(),操作结尾类似。
但差分不能完全描述字符串,还需要开头字符才能确定。因此考虑开头字符,可以发现只有操作开头会改变开头的字符,从差分的角度考虑,可以发现每次将 从 变为 时,会将 在模 意义下减少 ,另外一种操作会将 在模 意义下增加 。则开头字符的限制相当于这两种操作次数的差模 为定值。
考虑如何计算操作次数。首先考虑不在开头操作的情况,对于每一个 ,考虑初始状态和结束状态中前 个差分中 的个数的差 ,则显然至少需要交换第 个差分 次,才能使得前 个差分变为对应位置(对于最后一个位置,操作为改变这个位置的差分)。
那么最少操作次数为 ,可以发现这一定可以达到。例如,考虑每次找到第一个满足 的位置,不妨设 ,则这个位置的差分为 ,考虑从这个位置向右找到第一个差分为 的位置,则这一段中除去结尾差分均为 ,可以发现 ,从而将这个 移动过来的每一步都会减少 ,一直这样做即可。(如果上一步找到了结尾,则看成在结尾操作一次将差分变为 )
考虑开头有操作的情况,显然存在一种最优解,使得开头不同时存在 的操作,否则将操作的两个元素改为互相交换位置,一定不会增大操作步数。
设在开头进行了 次 的操作,沿用上面的过程,可以发现此时的最小步数不小于 ,沿用上述证明,看成在开头选择 次即可。如果进行了 次 ,则可以看成 次 ,则后面的步数不小于 。
因此可以看成选择一个整数 ,在 的开头加入一个 ,操作步数为 。而之前对于开头的限制可以看成 模 为一个定值。那么最后的问题为选择一个模 为定值的整数 ,最小化上一个式子。一种做法是从小到大考虑 间的所有 ,将 排序后可以均摊维护代价。还有一种做法是,由原函数的上凸性,在不考虑 限制时 的中位数为最优解。根据凸性,找到 的中位数,在它两侧取最接近的合法 判断即可。
复杂度 或者
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 500500
#define ll long long
char s[N],t[N];
int n,su[N];
int main()
{
scanf("%d%s%s",&n,s+1,t+1);
int tp=(t[1]-s[1]+3)%3-3*n-3;
for(int i=1;i<n;i++)
{
int v1=(s[i+1]-s[i]+3)%3,v2=(t[i+1]-t[i]+3)%3;
su[i]=su[i-1]-(v1==1?1:0)+(v2==1?1:0);
}
sort(su+1,su+n+1);
ll sa=0,sr=0,as=1e18;
for(int i=1;i<=n;i++)sa+=su[i];
while(tp<=n*3)
{
tp+=3;
while(sr<n&&su[sr+1]<=tp)sr++,sa-=su[sr]*2;
as=min(as,1ll*tp*(sr*2-n)+sa);
}
printf("%lld\n",as);
}
AGC052F Tree Vertices XOR
Problem
给一棵 个点的树,每个点有 间的权值。
你可以进行任意次如下操作:选择一个点 ,记所有与 相邻的点的点权异或和为 ,将 的点权异或 。
初始状态为所有点权为 ,求出初始状态可以到达的状态数量,答案模
Sol
通过简单观察,可以发现操作是可逆的,因此问题不一定需要局限于顺序操作,可以看成有多少个状态能到达全 ,或者能到达某个全 能到的状态。
可以发现一个 构成的连通块在不删完的情况下可以任意收缩(每次操作一个叶子),在与其它连通块不干扰的情况下也可以任意扩展(操作一个相邻点)。那么可以考虑将所有连通块缩成单点。同时在不干扰的情况下一个单点也可以任意移动:从 移动到相邻的 只需要先操作 再操作 。
初始状态显然可以操作到只有一个单点,因此问题可以看成,树上有若干个分离的单点 ,求能否将它们操作变为单个 。即能否将这些单点合并。
但树的形态使得问题非常复杂,考虑从简单情况开始分析。
首先考虑一条链,可以发现一条链上相邻两个单点一定无法合并:在它们只相隔一个点时就无法继续操作了。因此可以发现一个状态合法当且仅当它缩连通块后只剩一个单点,即初始状态只有一个连通块,因此答案为 。
然后考虑菊花,按照 的权值分类,值是 则可以直接缩成单点,否则可以发现合法当且仅当第一步能有效操作 ,从而答案是 ,但这个情况过于平凡。
回到链的分析,链上不能合并单点实际上相当于度数不超过 的点上不能进行合并或分离。这一结论是显然的:可以发现有效合并当且仅当一个 旁边有至少三个 ,因此至少需要度数为 。
因此接下来考虑有一个度数大于等于 的情况,即一个点连出若干条链。那么问题可以看成每条链上有若干个单点,可以在根上进行一些合并:如果当前根是 且有奇数个相邻点是 ,那么可以合并成一个单点放到任意一条链上。
这里合并有一个奇数的限制,从而不一定能完成操作。例如之前菊花的情况,如果初始不能进行任意操作,则显然无法达到目标。但可以发现,如果一条链上相邻两个 间有空位,即距离大于 ,则这条链上的点可以通过空位进行一些移动(例如 101001 -> 100101 -> 010101),那么此时只要链里面存在 ,它就可以任意调整与根相邻的点的权值,从而第一次合并一定可以进行。而第一次合并后,每个子树都有空位了,接下来的操作容易进行。
考虑什么情况下有空位。可以发现如果初始有一个连通块大小大于 ,缩连通块后就会存在空位。而如果初始一个点能合并,那合并后就会有空位。由此继续分析,可以得到如下性质:
- 如果初始时任意点都不能进行有效操作,则该状态显然不合法,否则一定可以找出自由移动的空位。(一个不能自由移动的空位的例子是每个 有两个 作为儿子)
证明:考虑操作的类型,如果是连通块的扩展和收缩那么可以存在一个连通块大小大于 ,否则合并起来显然也行。
猜想这对一般树也对,事实上大概也是这样的。但试一下可以发现只有这个条件对于上述情况都是不充分的。
重新考虑之前的过程,有空位之后的部分都没有讨论合并奇数个点的限制。再考虑这个限制,可以发现一条性质:
- 操作不改变连通块数量的奇偶性。从而合法的一个必要条件是连通块个数为奇数。
可以发现有了这两个条件就几乎很对了:第二个条件保证了最后一定能删完。
再试一次,猜想这对于一般树也对,把链判掉之后加上上面两个条件,然后AC62WA16
重新考虑之前的过程,考虑“接下来的操作容易进行”部分可能存在的问题。如果单点均匀分布在不同子树内,那么合并显然可以直接将单点合并完。但问题在于如果只有一个或者两个子树内有单点,就不能直接合并了。可以发现此时需要将一个子树内的点移到其它子树内,使得至少三个子树内有 1,才能进行合并。此时就可以发现问题:例如如果唯一一个三度点有两个子树都是单点,那么从另一边向这两个子树中放 ,放了一个后另一个就放不进去了。这说明度数 的点不一定能合并。
但可以发现,如果一个度数 的点有至少两个子树不是单点,那么一定可以合并:非单点的子树可以将一个 放到子树内更深处,从而不影响放到下一个单点。由此考虑将结论修改为:
如果树中存在点 满足其度数 且有至少两个子树大小 ,则一个状态合法当且仅当满足如下两个条件:
- 初始时至少一个点存在有效操作。
- 连通块个数为奇数。
由之前的整个过程,容易得到结论的证明:首先操作一次得到有空位的 ,然后将空位移动到 附近(如果有移动不了的点,那么可以先用空位的移动在这个点上进行一次合并)。现在考虑以 为中心进行合并,如果每个子树都是链,那么可以用之前的方式:在至少三个子树内存在 时直接拿上来合并,否则考虑从一个子树中拿 到其它子树,由之前的讨论这一定可以做到。对于任意子树,之前的分析实际上只需要要求所有 能一个一个拿出来。问题在于任意形态可能导致两个 同时连到一个 ,从而互相堵住的情况,但这种情况也容易解决:从外面拿一个 进来(第一次 上的操作后每个子树都有空位,因此可以这样做),移到第一个堵住的点上面,然后合并就可以解决这里的问题。
这种情况的计算是容易的:简单树形 dp 即可,一种状态设计是考虑填了子树内的情况,只有根的有效操作和子树外有关,因此设状态为当前子树的根权值为 ,其在父亲权值为 时存在有效操作,子树内除去根是否已经有存在有效操作的点,子树内连通块数量的奇偶性。中间两个状态也可以合并减少状态数。转移时枚举根的权值,将子树加进来的过程只需要记录是否出现了存在有效操作的点,同时记录根的状态和当前连通块数。连通块的计算可以点减边。复杂度 。
考虑最后留下来的情况:每一个度数 的点最多有一个儿子大小 。首先分析此时树的形态。考虑此时树的直径,那么不可能再有点连向直径端点(否则直径变长)。然后可以发现其它点只能连向端点旁边的两个点,否则连向更中间的点就满足了之前的限制。从而可以发现此时树形态为两个菊花被一条链连起来。
对这一情况进行分析,此时各种对菊花根处权值的分类讨论已经可以得到答案。另一种做法是如下结论:
可以将每一个菊花的根连出的所有单点合并为一个点,其点权为所有合并点的异或和,这样不改变合法性。
证明:充分性:合并后的树为一条链,其两端点权值为两个菊花分别连出单点的异或和,中间权值为原链上权值。可以发现原树上的每一次操作对应到新的链上也是一次合法操作(操作中间显然,操作菊花的根和叶子的情况也分别容易证明),那么合法则合并成链后必须合法。
必要性:考虑用链上的合法操作做树,操作端点时任意操作一个叶子即可,此时的问题是可能两侧叶子分别留下了偶数个 ,但把最后的单点分别移到两侧操作一下就可以处理这些 。
那么情况转化为链上的问题,如果菊花多一个叶子,可以发现这只会使合并后的点权值为 的方案数同时翻倍。因此记树的直径长度为 ,则答案是 。容易验证对于 很小的情况这也是对的。判定是否是这种情况也是容易的,例如可以求直径后判定。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 200500
#define mod 998244353
int n,a,b,head[N],cnt,dep[N],f1[N],d[N],fg;
struct edge{int t,next;}ed[N*2];
void adde(int f,int t)
{
d[f]++;d[t]++;
ed[++cnt]=(edge){t,head[f]};head[f]=cnt;
ed[++cnt]=(edge){f,head[t]};head[t]=cnt;
}
int dp[N][2][2][3],f[2][3],g[2][3];
void dfs(int u,int fa)
{
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs(ed[i].t,u);
for(int p=0;p<2;p++)
{
f[p][1]=1;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)
{
int t=ed[i].t;
for(int rv=0;rv<2;rv++)for(int su=0;su<2;su++)for(int fg=0;fg<3;fg++)
for(int ls=0;ls<2;ls++)for(int lf=0;lf<3;lf++)
{
int rs=ls^su^(p&rv),rf=lf^rv;
if(lf==2||fg==2||p==fg)rf=2;
g[rs][rf]=(g[rs][rf]+1ll*dp[t][rv][su][fg]*f[ls][lf])%mod;
}
for(int s=0;s<2;s++)for(int t=0;t<3;t++)f[s][t]=g[s][t],g[s][t]=0;
}
for(int s=0;s<2;s++)for(int t=0;t<3;t++)dp[u][p][s][t]=f[s][t],f[s][t]=0;
}
}
void dfs1(int u,int fa)
{
dep[u]=dep[fa]+1;f1[u]=fa;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs1(ed[i].t,u);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),adde(a,b);
dfs1(1,0);
int s1=1;for(int i=2;i<=n;i++)if(dep[i]>dep[s1])s1=i;
dfs1(s1,0);
int s2=1;for(int i=2;i<=n;i++)if(dep[i]>dep[s2])s2=i;
for(int i=s2;i;i=f1[i])if(dep[i]!=dep[s2]-1&&dep[i]!=2&&d[i]>2)fg=1;
if(!fg)
{
int l=dep[s2],as=1ll*l*(l+1)/2%mod;
for(int i=1;i<=n-l;i++)as=as*2%mod;
printf("%d\n",as);
return 0;
}
dfs(1,0);
printf("%d\n",(1ll*dp[1][0][1][0]+dp[1][0][1][2]+dp[1][1][1][0]+dp[1][1][1][2])%mod);
}
AGC051C Flipper
Problem
有一个 的网格,网格上有 个格子是黑色,剩余格子是白色。
你可以进行如下操作:选择一个 的矩形区域,翻转矩形内所有格子的颜色。
你需要最小化最后的黑色格子数量,求出可能的最小黑色格子数量。
Sol
以下认为白色表示 ,黑色表示 。
考虑以 为左上角操作一次,以 为左上角操作一次,这样会翻转四个格子 。
则如果把所有列按照模 分类,每一轮单独组成一个网格,则上述操作相当于在某一类的网格上翻转一个 。可以发现每一类上的任意一个 矩形都可以用这种方式翻转。
考虑翻转 的问题。记第 行 数量的奇偶性为 ,第 列 数量的奇偶性为 。可以发现翻转 的矩形不改变 。而考虑每一行可以得到 至少有 个,考虑每一列可以得到 至少有 个。因此 数量的下界是 。
而对于翻转 的问题,可以发现无论初始状态如何,都可以通过翻转任意决定除去第一行第一列以外的格子的颜色。从而可以得到如下结论:
对于任意两种 相等的局面。可以通过翻转 从一种到达另外一种。
证明:考虑异或两个局面,只需要证明如果 ,则可以从当前局面翻转到全 。考虑从右下往左上,将除去第一行第一列以外的格子全部变成 。此时由于 ,可以得到除去第一行第一列格子外所有格子都是 ,进而这个格子也是 ,这就达到了目标。
那么对于一种 ,考虑如下局面:
设 的位置为 , 的位置为 。对于 ,将 位置设为 。如果剩余一些 ,将对应的 设为 ,剩余 则将 设为 。
可以发现这样满足 的限制,且使用的 数量为 。这样达到了下界。
回到原问题,翻转操作可以交换顺序,因此可以看成先做一些其它操作,最后做上述操作,最小化上述操作的代价。
注意到翻转 不影响最后每一列的 ,因此三个矩形上 都是定值。
而翻转 会使得每一类上这两行的 全部翻转。因此操作可以看成选择相邻两行,使得三个矩形上这两行的 全部翻转。
考虑每一行有没有被翻转,可以发现能操作到的状态一定满足翻转的行数是偶数,且只要行数是偶数,就可以通过翻转得到。
那么记每一行上三个矩形的 分别为 ,问题变为如下形式:
给定 个 三元组 ,你可以选择一些三元组,翻转内部所有元素,要求选择的个数为偶数。
在选择后,记 为所有三元组中 的和。给定 ,你需要最小化 。
考虑贪心,先把所有三元组翻转为 ,在此基础上做翻转,此时需要翻转的个数满足某一奇偶性。
此时如果翻转了两种不同的三元组,则将两个翻转同时撤销,可以发现 都不会变大,从而这样更优。因此只需要考虑翻转一种三元组的情况。此时代价关于翻转数量是一个上凸函数,那么可以找到最优解再在附近找满足奇偶性的最优解。
使用 map
即可求出所有的 ,复杂度
Code
#include<cstdio>
#include<map>
#include<algorithm>
using namespace std;
int n,x,y,li[3],ci,cr[3],as;
map<int,int> s1,si;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x,&y);
s1[y]^=1;
si[x]^=1<<(y%3);
}
for(map<int,int>::iterator it=s1.begin();it!=s1.end();it++)
if(it->second)li[(it->first)%3]++;
for(map<int,int>::iterator it=si.begin();it!=si.end();it++)
{
int vl=it->second;
if((vl&-vl)!=vl)vl^=7,ci++;
if(vl)cr[vl-(vl==4)-1]++;
}
for(int i=0;i<3;i++)as+=max(li[i],cr[i]+(ci&1));
for(int i=0;i<3;i++)if(cr[i])
{
int l1=cr[i]-li[i];
for(int j=0;j<3;j++)if(i!=j)l1=min(l1,li[j]-cr[j]);
if(l1<0)l1=0;
for(int t=l1-3;t<=l1+3;t++)if((ci+t)%2==0&&t>=0&&t<=cr[i])
{
int tp=max(li[i],cr[i]-t);
for(int j=0;j<3;j++)if(i!=j)tp+=max(li[j],cr[j]+t);
as=min(as,tp);
}
}
printf("%d\n",as);
}
AGC051D C4
Problem
有一个四元环,点标号为 。你需要从 出发,走一条可以非简单的路径,最后回到 。
给定 ,你的路径需要满足 的边正好经过 次。求合法路径数,答案模
Sol
考虑取 作为关键点,则路径必定每两步经过一个关键点。那么路径可以分为经过两个关键点的路径()和在非关键点折返的路径。
考虑枚举 路径出现的次数 以及 路径出现的次数 ,则只考虑这两种路径时合法当且仅当 是偶数,且方案数为 (一定在两个关键点间来回)。
现在考虑折返路径,从 出发的折返路径有两种,分别是 ,它们分别有 条,从 出发的折返路径有两种,分别有 条。
可以发现从 出发的折返路径只需要考虑所有经过 的情况,之前有 次经过 ,从而加入两种折返路径的方案数为 。经过 的路径类似,但经过 的次数不需要加一。
最后的方案数即为枚举 ,将三部分的组合数相乘并求和得到答案,这里如果出现不能整除的情况认为权值是 。
可以发现乘积可以分成若干阶乘和阶乘逆元,它们可以分成三部分,每一部分分别只和 有关。因此可以将 部分分别看成一个生成函数,相乘后再处理 的贡献。
复杂度
好像可以线性,但是🕊了。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1050000
#define mod 998244353
int n=1e6,s1,s2,r1,r2,fr[N],ifr[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int rev[N*2],gr[2][N*2];
int C(int x,int y){return 1ll*fr[x]*ifr[x-y]%mod*ifr[y]%mod;}
void init(int d=20)
{
for(int l=2;l<=1<<d;l<<=1)for(int i=1;i<l;i++)rev[l+i]=(rev[l+(i>>1)]>>1)|((i&1)*(l>>1));
for(int t=0;t<2;t++)
for(int l=2;l<=1<<d;l<<=1)
{
int tp=pw(3,(mod-1)/l),vl=1;
if(!t)tp=pw(tp,mod-2);
for(int i=0;i<l;i++)gr[t][i+l]=vl,vl=1ll*vl*tp%mod;
}
}
int f[N],g[N],ntt[N];
void dft(int s,int *a,int t)
{
for(int i=0;i<s;i++)ntt[rev[s+i]]=a[i];
for(int l=2;l<=s;l<<=1)
for(int i=0;i<s;i+=l)
for(int j=0;j<l>>1;j++)
{
int v1=ntt[i+j],v2=1ll*ntt[i+j+(l>>1)]*gr[t][l+j]%mod;
ntt[i+j]=(v1+v2)%mod;
ntt[i+j+(l>>1)]=(v1+mod-v2)%mod;
}
int inv=t?1:pw(s,mod-2);
for(int i=0;i<s;i++)a[i]=1ll*ntt[i]*inv%mod;
}
int main()
{
fr[0]=1;for(int i=1;i<=n;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[n]=pw(fr[n],mod-2);for(int i=n;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
init();
scanf("%d%d%d%d",&s1,&s2,&r1,&r2);
if((s1+s2)%2||(r1+r2)%2||(s1+r1)%2){printf("0\n");return 0;}
int ls=min(s1,s2),lr=min(r1,r2);
init();
int l=1;while(l<=ls+lr)l<<=1;
for(int i=ls;i>=0;i-=2)f[i]=1ll*ifr[i]*ifr[(s1-i)/2]%mod*ifr[(s2-i)/2]%mod;
for(int i=lr;i>=0;i-=2)g[i]=1ll*ifr[i]*ifr[(r1-i)/2]%mod*ifr[(r2-i)/2]%mod;
dft(l,f,1);dft(l,g,1);for(int i=0;i<l;i++)f[i]=1ll*f[i]*g[i]%mod;dft(l,f,0);
for(int i=2;i<=ls+lr;i+=2)
as=(as+1ll*f[i]*fr[i]%mod*fr[(s1+r2-i)/2]%mod*fr[(s2+r1-i)/2]%mod*C((s1+r2)/2,i/2)%mod*C((s2+r1)/2-1,i/2-1))%mod;
printf("%d\n",as);
}
AGC051E Middle Point
Problem
二维平面上当前有 个点,它们坐标都是整数。保证不存在三点共线。
你可以进行若干次操作,每次操作你可以选择两个当前在平面上的点,并在平面上加入这两个点连线的中点。
求平面上最多能有多少个整点。
Sol
首先考虑如何判断一个点能否出现,将这个点平移到原点,考虑能否通过操作使原点出现。
即有 个二维向量 ,每次操作可以选择两个向量 ,加入 。考虑加入的向量具有什么形式,可以得到加入的向量一定可以表示为 ,其中 为非负整数,且 。如果取中点的两个向量原先具有这种形式,则容易得到 具有这种形式,这样就证明了上一个结论。同时,对于一个这样的形式,考虑将 看成有 个点,然后做 次操作每次配对合并,这样可以发现任何一个这样形式的向量都可以表示出来。因此得到如下结论:
出现当且仅当存在非负整数 使得 且 是某个 的整数次幂。
此时可以发现如下结果:
- 对于一个在给定点组成的凸包之外的点,它显然不可能出现。
- 对于一个在给定点组成的凸包边界上的点,在上述过程中选择 时,可以发现只有这个点所在的凸包边上的两个点对应的 可以不是 。此时的情况容易解决。
接下来考虑点在凸包严格内部的情况。首先考虑三个向量 的情况,如果 ,做叉乘可以得到:
则可以发现 。因此对于三个向量 ,可以让对应的 分别加上 的值(除以值的 )的某个倍数,这样总和还是 。
如果直接加可能违反非负的限制。但此时原点在凸包的严格内部,考虑找到三个点 使得它们组成的三角形包含原点且原点不在边界上,则在这三个点上使用上述操作会让三个 都增加,因此这三个 可以任意大。接下来考虑剩余的点,选择一个这样的点 以及它到原点的连线与三角形相交的那条边上的两个点 ,则可以发现操作会增大 对应的 ,减小 对应的 。依次考虑所有点,可以得到一组正整数 ,使得 。
考虑能否找到三角形。考虑所有点的极角,则至少有三种不同的极角,且排序后相邻两种角度差小于 (否则当前点不在凸包内)。将一个角度与相反的角度配对。如果有一个角度没有配对,则从这个角度开始,每次选择顺时针方向最后一个距离它小于 的角度,可以发现选择的第三个角度一定至少旋转了 ,可以发现这样满足要求。剩下的情况不一定能找到这样的三角形,但此时所有角度都被配对了,可以发现此时一定能找到一组正整数 ,使得 。
因此这种情况中一定存在这样的一组正整数 ,设此时 。考虑在上述问题中去除非负的限制,此时的一组解中可能有负数 。设此时的和为 ,最大的负数为 。考虑将这两组解做整系数线性组合,则可以得到所有 的解,同时如果 则组合出的所有 都是非负整数。考虑找到一组合法的非负解,即 是某个 的幂,且 。不妨设 为奇数(否则考虑将 除以 , 减一,这不影响 是否是 的幂,而如果当前 ,则考虑将之前的解所有数乘二再做这个过程),则由欧拉定理,,考虑取 , 满足 ,则对于任意正整数 这样得到的 都是整数,且只需要取足够大的 就可以满足 。因此,原问题有一组 非负且 的解当且仅当去掉非负条件有解。
而去掉非负条件后,就可以任意选择 进行之前的操作。可以发现这样的操作一定可以使总和增加 的大小。可以发现这相当于 的大小,即这三个点构成的三角形面积的两倍,这与原点的选择无关。考虑对于所有选择三个给定点的方式计算这个值,则可以通过这些操作使 为这些值的任意整系数线性组合。显然,设所有这些值的 为 ,则它们的整系数线性组合可以表示所有 的倍数。因此容易得到如下结论:
考虑对于每三个点计算它们构成的三角形面积的两倍,对所有值求 ,如果 为 的整数次幂,则给定点组成的凸包内部所有点都可以出现。
接下来考虑不满足这个条件的情况,此时存在一个 的奇数 ,使得任意三个点满足 的大小都是 的倍数。这个 即为上面的 去除质因子 的结果。此时不一定所有点都能表示,但可能有一些点能表示出来(例如,如果将一个点移动到原点后满足 的值都是 的倍数,则选择这一对时可以加上 倍的面积)。
考虑此时所有点的性质。将一个给定点放到原点,则此时任意两个点 需要满足 (这里不要求两个点不相同)。考虑 是奇质数的情况,如果存在一个 不是 的倍数,设这个点是 ,则对于任意一个点 ,都有 ,即存在一个数 ,使得对于任意一个点都有 。考虑 的情况,其中 为奇质数。此时类似地可以得到,如果存在一个 不是 的倍数,则存在一个数 ,使得对于任意一个点都有 。对于更一般的情况,考虑每个质因子,然后使用 CRT 合并,可以得到类似结论。
因此考虑所有点的 的 ,设这个值为 ,设 去除质因子 后的值为 。则显然 是 的倍数。如果将所有的 除以 ,则变换后 也会除以 。此时由上述分析有如下结论:
设有一个给定点为原点,如果所有 的 是 的次幂,由上一段中的方法计算 ,则存在一个 ,满足对于所有给定点 都有 。
此时相当于存在两个向量 ,使得任意一个给定点可以被表示为 ,且 为整数。对于除 之前的情况,可以看成两个向量为 。这里 都是奇数。
注意到第一部分证明了在操作过程中,能放上去的点都形如 ,其中 为整数。如果将 作为新的基底,则当前能放上去的点都形如 。如果它是整点,则考虑横坐标可以发现 必须是整数,再考虑纵坐标可以发现 必须是整数。因此可以得到一个放上去的点在坐标变换后是整点当且仅当它在变换前是整点。那么变换后的答案即为变换前的答案。
因此考虑变换所有给定点,如果一个给定点满足 ,则变换后坐标为 。可以发现变换后一次叉乘的结果一定是之前的 倍(变换后坐标可以看成 ,由叉乘公式可以得到),因此这样变换后,由第一段中方式求出的 一定是 的整数次幂。此时就可以使用上述结论完成这部分,只需要求出一个凸包内部的整点数即可。由于凸包的所有顶点都是整点,此时可以使用皮克定理,即简单格点多边形的面积等于内部整点数加上边界整点数的一半减一,而面积可以求凸包后直接得到,边界整点数也可以对凸包的每条边直接考虑,这样即可得到内部整点数。当然也可以直接类欧算floor_sum做
最后再考虑凸包边界上每条边的情况。对于一条边,如果将它看成向量后为 ,则这条边上有 个整点。考虑这条边上能放多少个整点,此时只需要考虑这条边两个端点的的操作,因此只需要考虑边上的情况。设 为 ,则如果将这条边的两个点看成 ,则所有整点分布在 。由第一部分的结论,不难得到此时能表示一个 间的数当且仅当它可以被表示为 的形式,其中 为整数。那么不难得到这条边上可以放上去的整点数为 。因此对于每条边计算 并求和即可得到边界上能加入的边数。可以发现,上一步中的变换不会影响一条边的 ,因此可以在变换后求一次凸包并同时求两部分的贡献。
最后考虑如何求出 ,每个方程相当于 ,可以用一些处理消去 ,然后对 个方程组求 excrt
即可。
这样就得到了一个 或 的做法,瓶颈在于求出 (直接枚举是 ,但可以发现固定一个点的所有叉乘可以线性组合出其它叉乘)
下面是一个(我编的) 做法:
考虑先将所有的 除去它们的 (质因子 部分不动),此时由上述结论,如果任意三个点进行 得到的所有结果的 为 ,,则存在 满足对于所有给定点都有 。而另一方面,如果存在 使得所有给定点都满足 ,则显然所有 的结果都是 的倍数。
之前的做法是得到 后 excrt
解出 ,现在考虑依次加入点,维护当前解出的 以及 ,在加入点时如果方程无解则减小 ,由上述性质显然新的 是之前 的约数。
具体来说,考虑之前求 的过程,对于一个点 ,设 ,则可以得到方程 。在加入一个新的点时有如下情况:
- 不能整除 。
可以发现对于真正的 来说一定有 ,因此需要去掉当前 的一些因子。一种做法是不断让 除以 并更新 ,直到 。
考虑将 除以某个数后之前的方程的变化。对于一个方程 , 变为某个约数后逆元可以不变,因此只会改变最后的模数部分。因此这样相当于将每个方程的模数缩小为一个约数。因此可以发现原来的解仍然是解,因此现在解形如 ,其中 为当前所有方程模数的 ,可以发现除以 后新的 为 。
excrt
的过程无解,即合并两个方程 时 不能整除 。
此时同样考虑减小 ,如果 除去了一个质因子 ,由上一条可以发现如果第一个方程模数中包含 ,则该方程中模数会除以 。而第二个方程的模数是 ,可以发现它也满足这个性质。则至少需要将 除以 (因为 中多出了这些不能整除 ),且可以发现这样后方程就满足了条件。
这样考虑了所有数后可以得到一个 以及一个关于 的同余方程。由于 互质,因此所有的 的 一定是 ,这样就找到了一组 。且上面过程中每一步都可以说明 不能更大,因此这就是要求的 。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 105
#define ll long long
int n,s[N][2];
struct pt{int x,y;}p[N],st[N];
pt operator -(pt a,pt b){return (pt){a.x-b.x,a.y-b.y};}
ll cross(pt a,pt b){return 1ll*a.x*b.y-1ll*a.y*b.x;}
bool operator <(pt a,pt b){return cross(a,b)>0;}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(!b){x=1;y=0;return a;}
ll g=exgcd(b,a%b,y,x);y-=a/b*x;return g;
}
ll getinv(ll a,ll p)
{
ll x,y;
exgcd(a,p,x,y);
return (x%p+p)%p;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&s[i][0],&s[i][1]);
int fr=1;
for(int i=2;i<=n;i++)if(s[i][0]<s[fr][0]||(s[i][0]==s[fr][0]&&s[i][1]<s[fr][1]))fr=i;
swap(s[1][0],s[fr][0]);swap(s[1][1],s[fr][1]);
ll gt=0;
for(int i=n;i>=1;i--)s[i][0]-=s[1][0],s[i][1]-=s[1][1],gt=gcd(gt,s[i][0]);
gt/=gt&-gt;
for(int i=1;i<=n;i++)s[i][0]/=gt;
ll ri=1ll*s[2][0]*s[3][1]-1ll*s[3][0]*s[2][1];ri/=ri&-ri;if(ri<0)ri=-ri;
ll vi=0,pi=1;
for(int i=2;i<=n;i++)
{
ll g1=gcd(ri,s[i][0]);
ll tp=g1/gcd(s[i][1],g1);if(tp<0)tp=-tp;
while(tp>1)ri/=tp,g1=gcd(ri,s[i][0]),pi/=gcd(pi,tp),tp=g1/gcd(s[i][1],g1),tp=tp>0?tp:-tp;
ll p1=ri/g1,v1=(__int128)s[i][1]/g1*getinv(s[i][0]/g1,p1)%p1;
ll g=gcd(pi,p1);
tp=g/gcd(g,v1-vi);if(tp<0)tp=-tp;
p1/=tp;pi/=tp;vi%=pi;v1%=p1;g/=tp;ri/=tp;
ll rs=vi+(__int128)(v1-vi)/g*getinv(pi/g,p1/g)*pi;
pi*=p1/g;vi=(rs%pi+pi)%pi;
}
for(int i=2;i<=n;i++)s[i][1]=(s[i][1]-(__int128)s[i][0]*vi)/ri,p[i]=(pt){s[i][0],s[i][1]};
int ct=0;
sort(p+2,p+n+1);
st[ct=1]=p[1];
for(int i=2;i<=n;i++)
{
while(ct>1&&cross(p[i]-st[ct-1],st[ct]-st[ct-1])>=0)ct--;
st[++ct]=p[i];
}
ll as=2;
for(int i=1;i<=ct;i++)
{
pt sl=st[i],sr=st[i%ct+1];
as+=cross(sl,sr);
ll g=gcd(sl.x-sr.x,sl.y-sr.y);
if(g<0)g=-g;
as+=(g&-g)*2-g;
}
printf("%lld\n",as/2);
}
AGC051F rng_58's Last Problem
Problem
你有两个沙漏,它们分别可以测量 个单位时间,求你是否能用这些沙漏精确测量 个单位时间,这里 。
具体来说,在时刻 ,你可以选择一些沙漏翻转,之后每次有沙漏停止时你可以选择一些沙漏翻转,你需要使得 时刻有一个沙漏正好停止。
多组数据,
Sol
称两个沙漏都为空的状态为初始状态。考虑一个沙漏停止且另外一个沙漏没有停止时的操作。如果此时不翻转已经停止的沙漏,则另外一个沙漏停止后就会变为初始状态。
考虑剩余的两种操作,如果每次只翻转当前已经停止的沙漏,则两个沙漏是独立的,因此它们表示出的所有时刻为将所有非负整数以及所有 的倍数排成一列的数列,即 。记这个序列为 。
考虑另外一种操作,可以发现两个沙漏一起翻转相当于时光倒流,此时可以发现如果只考虑这两种操作,则它们可以看成如下形式:
所有的 在数轴上排成一列,称数轴上的这些位置为点。你初始在 ,向右移动。你每经过一个点时可以选择转向或者不转向,总用时即为你的总路程。这里不转向表示只翻转停止的沙漏,转向表示翻转两个沙漏。
但在原问题中,可以在走到一个状态时选择另外两种操作回到初始状态,并重新开始行走。此时有如下结论:
对于任意一种从初始位置出发走出任意条路径的方案,存在一组路径满足路径长度和与给定的路径长度和相同,且这组路径除去某一条外,剩余路径长度均为 或 。
证明:找到经过的最大位置最大的一条路径,考虑将剩余路径拼接到这条路径上。对于一条路径,设它的终点为 ,则 中的每一段 一定经过了奇数次,后面的每一段经过了偶数次。注意到如果一段经过了两次,则可以将其中的两次提出来,看作一个环(),拼接到第一条路径上,因为第一条路径经过的最大坐标最大,因此一定可以拼接上去。此时剩余部分为从 到 的每一段各一次,但这部分的长度一定为 或 ,因此可以将这部分拆成若干个 和 。
而如果一组路径除去一条外只有 ,则可以先处理所有 ,最后再走长的路径,这样就不需要考虑用什么方式结束路径的问题。
因此对于一个 ,问题变为能否找到一条路径长度为 ,且 。
考虑路径的形式,设路径经过的最右侧点位置为 ,结束点为 ,考虑取出 的一条路径,则每一段额外经过的次数一定是偶数次,因此剩下的部分可以分解为一些只经过一段的环。因此路径总长可以看成 ,这里 表示额外经过 的环的数量。
考虑只有后面部分的问题,即对于一个 以及一个 ,是否可以找到一组非负整数 以及非负整数 使得 。如果 显然有解。考虑 的情况,另外一种类似。
每一个 都可以被表示为 的形式。显然在 时,只有 的 有用(选择其余的一定不优)。考虑这样的元素,可以发现对于每一个正整数 ,有一个 ,记这个值为 。则只会使用这些 ,问题可以看成找到一组 使得
考虑哪些元素一定是不需要的,可以发现如果对于 有 ,则可以发现 一定等于 ,因此如果在一组解中 大于 ,可以将 加上对应值,然后将 变为 。考虑 的系数可以发现在任意一组解中只有前 个 可以非零,从大到小用上述方式考虑每个元素即可得到如下结论:
可以只使用那些满足 是 的前缀最小值位置的 表示出所有的方案。
而如果 是 的前缀最小值,则 一定是序列 的前缀最小值。注意到 ,可以发现这个序列的前缀最小值相当于对 做有理逼近。
考虑在 Stern-Brocot Tree 上逼近 的过程,注意到 的连分数表示为 ,对应到 Stern-Brocot Tree 上,可以发现这相当于在逼近 时,会先向右一步,接下来重复这个过程:向左两步,向右两步。这个过程可以得到如下序列:,其中所有 的有理数 对应了这里的一个 ,所有 的有理数对应另外一侧问题中需要的所有元素。
可以发现有用的元素只有 个,结合最后的一个结论可以写一个背包暴力以v2/3或者v1/3的复杂度AC
在二维平面上考虑这个问题(先不考虑前面 的系数),将一个 看作点 。则相当于当前有一些向量 ,你初始在原点,每次可以选择一个向量以这个向量进行移动,求你能否到达一个点,使得 在这个点的左上方。
可以发现这些向量作为 的上逼近有非常好的性质,例如如下性质:
在这一列向量中,相邻两个向量叉乘的大小为 。
证明:考虑之前的逼近序列 ,由 Stern-Brocot Tree 的构造方式不难发现此时的两个相邻元素 满足 。考虑在此时不相邻但只考虑 时相邻的两个元素(例如 ),如果将构造方式写成向量加法,则可以发现构造形式为 ,可以发现 ,因此它们的叉乘大小也是 。
此时可以得到如下结论:
如果只使用前 个向量 ,则一个整点 可以被达到当且仅当它在直线 的上方或者在直线上。
证明:考虑对 归纳, 显然成立。考虑从 到 的过程。由逼近过程中 递减显然可以得到充分性,接下来证明必要性。
找到一个正整数 使得 ,则 ,因此 组成的三角形大小为 ,但这个三角形的边界上有 个整点,因此它的内部没有其它整点。
考虑 的这一段,由归纳这一段中在直线 上方的都可以被前 个向量表示,而由上述分析 时在 上但不在上一条直线上的只有一个点 ,而它可以被第 个向量到达。因此在 时结论正确,之后的部分可以用第 个向量转为 的情况。
此时可以发现它们能到达的区域只会变化 次,且每次都是当前最后一个上述向量形成的直线。
回到原问题,对于一个 ,判断后半部分能表示出的数只需要考虑前 个中最后一个在逼近中出现的向量。考虑枚举这个向量,设它对应的位置为 ,则对 的限制为 。可以发现判断合法只需要考虑如下四种情况:
- 可以选择 ,或者 后面第一个 的倍数。
- 在此基础上, 可以选择 ,也可以选择 前面第一个和 类型不同的数。
可以发现对于这一段中剩余的 选择,都可以通过减去若干个 得到这四种中的一种,而显然这四种更优。
对于一种情况,只需要判断减去 后当前数能否被前 个关键向量表示出,使用上述结论即可 判断。最后枚举所有 即可。
对于另外一侧( 的情况),可以发现上一部分的结论仍然成立,因此该做法同样正确,对两部分分别做一次即可。
复杂度
一些有趣的结论:如果将小数部分看成纯随机(事实上无理数有个无限稠密定理,但我不会用),则在前 个 中最小数的期望是 的,则它的斜率比 多出 。由之前的分析,取前 个关键向量时,只需要看点 是否在最后一个关键向量形成的直线上方。如果只在前 个数中取关键向量,则在 处关键向量组成的直线与 的高度差为 。但由 部分可以发现,在前 个数中取关键向量需要额外 的代价,从而只需要考虑 不超过 的情况,之后的部分一定不优。事实上 时只考虑 的部分可以保证正确性。因此完全可以只保留小的数做背包,最坏复杂度也只是v^2/3
Code
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define N 105000
#define ll long long
vector<pair<int,int> > vl,vr;
void solve(int a,int b,int c,int d)
{
if(b>950||d>950)return;
int sa=a+c,sb=b+d;
int fg=1ll*sa*sa>=2ll*sb*sb;
if(fg)vl.push_back(make_pair(sa,sb)),solve(a,b,sa,sb);
else vr.push_back(make_pair(sb,sa)),solve(sa,sb,c,d);
}
int n,a,b,is[N];
struct sth{int a,b,id;};
vector<sth> sl,sr;
void rsolve(vector<pair<int,int> > vi,vector<sth> qi)
{
for(int i=0;i<vi.size();i++)
{
int lx=vi[i].first,ly=vi[i].second;
for(int j=0;j<qi.size();j++)
{
sth nw=qi[j];
int v1=nw.a-lx,v2=nw.b;
if(v1>=0)v1/=2,v2=(v2+1)/2,is[nw.id]|=1ll*ly*v1>=1ll*v2*lx;
v1=nw.a,v2=nw.b+ly;
if(v1>=0)v1/=2,v2=(v2+1)/2,is[nw.id]|=1ll*ly*v1>=1ll*v2*lx;
v1=nw.a-lx*2,v2=nw.b-ly;
if(v1>=0)v1/=2,v2=(v2+1)/2,is[nw.id]|=1ll*ly*v1>=1ll*v2*lx;
v1=nw.a+lx-1,v2=nw.b+ly*2;
if(v1>=0)v1/=2,v2=(v2+1)/2,is[nw.id]|=1ll*ly*v1>=1ll*v2*lx;
}
}
}
int main()
{
solve(0,1,1,0);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&b);
if(a>=0&&b>=0)is[i]=1;
else if(a>=0)sl.push_back((sth){a,-b,i});
else sr.push_back((sth){b,-a,i});
}
rsolve(vl,sl);rsolve(vr,sr);
for(int i=1;i<=n;i++)printf("%s\n",is[i]?"Yes":"No");
}
AGC050D Shopping
Problem
有 个人和 个物品,使用如下方式分配这些物品:
重复进行如下操作,直到所有物品被拿走:
按照编号从小到大考虑每个人。考虑到一个人时,如果他已经拿走了一个物品,则不进行操作。否则,他需要从自己之前没有选择过的物品中随机选择一个。如果选择的物品没有被其它人拿走则当前人拿走这个物品,否则不进行任何操作。
对于每个 ,求出编号为 的人拿走物品的概率,答案模 。
Sol
可以发现,当前人的操作只和当前的轮数(决定剩余选择数量)以及之前被其它人拿走的物品数量有关。
看成拿走物品的人直接离开,考虑直接 dp
,设 表示还剩 个人,当前轮到第 个人选择,之前进行了 轮,还剩 个物品没有被选走,此时的第 个人拿走物品的概率。考虑当前操作的人是否能够拿走物品即可直接转移。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 43
#define mod 998244353
int n,k,ir[N];
int dp[N][N][N][N][N];
bool vis[N][N][N][N][N];
int dfs(int n,int u,int x,int a,int b)
{
if(!b)return 0;
if(u==n+1)return dfs(n,1,x,a-1,b);
if(vis[n][u][x][a][b])return dp[n][u][x][a][b];
int as=0;
if(a)as=(as+1ll*ir[a+b]*a%mod*dfs(n,u+1,x,a,b))%mod;
if(b)
if(u==x)as=(as+1ll*ir[a+b]*b)%mod;
else as=(as+1ll*ir[a+b]*b%mod*dfs(n-1,u,x-(u<x),a+1,b-1))%mod;
vis[n][u][x][a][b]=1;
return dp[n][u][x][a][b]=as;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=k;i++)for(int j=0;j<i;j++)if((1ll*j*mod+1)%i==0)ir[i]=(1ll*j*mod+1)/i;
for(int i=1;i<=n;i++)printf("%d\n",dfs(n,1,i,0,k));
}
AGC050E Three Traffic Lights
Problem
有三个交通信号灯,第 个灯会重复如下过程:亮绿灯 秒,再亮红灯 秒。
当前所有灯刚好同时变为绿灯,求在接下来的 秒中,有多少秒满足三个灯都是绿灯。答案模 。
Sol
可以发现问题相当于求有多少个 满足如下限制:
令 ,则相当于有三个方程 。
考虑化简问题的形式。首先,令 。考虑 的情况,此时 都是 的倍数,考虑将时间每 秒分成一段,即令 ,此时可以发现一个方程变为 ,即 。这样对于一个固定的 ,接下来的问题变为满足 的情况。可以发现一个方程变换后的结果只和 与 的大小关系相关。因此三个 将可能的 取值分为四段,每段内接下来的问题相同。那么对于 的情况,可以转化为求 个 的问题的答案。
如果 两两互质,则三个 相互独立,每组余数都可以由 CRT
得到对应的 。考虑一个元素,如果 与 互质,则 和剩下两个余数的结果独立,此时可以变为求出只考虑剩余两个方程情况的答案,再乘以 。
考虑更一般的情况,即 且 。此时考虑将 间的 写成 的形式,其中 。那么 只由 决定, 由 共同决定。
对于一个 考虑所有 ,可以发现此时得到的所有 是所有满足 且在 间的数。因此一个 对答案的贡献为 。因此 的贡献会分成两段,贡献加一的条件是 ,因此这种情况可以变为计算两种形式相同问题的答案,一个问题中删去了第一个方程,另外一个问题中将第一个方程变为了 ,此时的 满足 。
考虑对所有分出的问题进行上述处理,直到不满足上述条件。可以发现这样只会处理有限次,且只会分出 个子问题。(这里只需要说明对于每个元素令 ,这样做一轮后就不需要再做了,而考虑每一个质因子的幂次容易证明)
考虑此时分出的问题的形式。此时的问题满足 且对于任意一个 ,都有 。可以发现如下结论:
此时对于一个得到的问题,一定存在 使得 且 互质。
考虑计算 的答案,此时 可以看成 个区间,要求合法的 属于这些区间中的一个。对于另外两个现在,可以类似地得到 个区间。
区间数量可能是 的,但可以发现如下结论:
中除去最大的数外,剩余数不超过 。
因为 ,因此这显然成立。
不妨设 最大,那么考虑前两个方程分别得到的 个区间,求出这两组区间的并,这里只有 个区间。接下来只需要求出这些区间中由多少个 满足 。可以发现一个区间中满足后面条件的 数量可以 计算,因此单个问题可以 解决。
复杂度
Code
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
#define mod 998244353
ll v1,l1,v2,l2,v3,l3;
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
struct sth{ll l,r;};
vector<sth> doit(vector<sth> s1,vector<sth> s2)
{
vector<sth> as;
int l1=0,l2=0;
ll nw=0;
while(l1<s1.size()&&l2<s2.size())
if(s1[l1].r<nw)l1++;
else if(s2[l2].r<nw)l2++;
else if(s1[l1].r<s2[l2].l)l1++;
else if(s2[l2].r<s1[l1].l)l2++;
else as.push_back((sth){max(s1[l1].l,s2[l2].l),min(s1[l1].r,s2[l2].r)}),nw=min(s1[l1].r,s2[l2].r)+1;
return as;
}
int solve(ll va,ll la,ll vb,ll lb,ll vc,ll lc)
{
ll g=gcd(gcd(la,lb),lc),as=0;
if(g>1)
{
ll mi=min(min(va,vb),vc);
vector<ll> si;
si.push_back(-1);si.push_back(va%g);si.push_back(vb%g);si.push_back(vc%g);si.push_back(g-1);
sort(si.begin(),si.end());
for(int i=0;i<4;i++)if(si[i+1]<=mi)as=(as+(si[i+1]-si[i])%mod*solve((va-si[i+1])/g,la/g,(vb-si[i+1])/g,lb/g,(vc-si[i+1])/g,lc/g))%mod;
return 1ll*as*(g%mod)%mod*(g%mod)%mod;
}
ll tp=la/gcd(la,lb)/gcd(la,lc);
if(tp>1)return ((va/(la/tp)*(la/tp))%mod*solve(0,1,vb,lb,vc,lc)+solve(va%(la/tp),la/tp,vb,lb,vc,lc))%mod;
tp=lb/gcd(la,lb)/gcd(lb,lc);
if(tp>1)return ((vb/(lb/tp)*(lb/tp))%mod*solve(va,la,0,1,vc,lc)+solve(va,la,vb%(lb/tp),lb/tp,vc,lc))%mod;
tp=lc/gcd(lc,lb)/gcd(la,lc);
if(tp>1)return ((vc/(lc/tp)*(lc/tp))%mod*solve(va,la,vb,lb,0,1)+solve(va,la,vb,lb,vc%(lc/tp),lc/tp))%mod;
ll p=gcd(la,lc),q=gcd(la,lb),r=gcd(lb,lc);
while(p>1e6||q>1e6)swap(p,q),swap(q,r),swap(va,vb),swap(vb,vc);
vector<sth> sp,sq;
for(int i=0;i<p;i++)sp.push_back((sth){i*q*r,i*q*r+vb});
for(int i=0;i<q;i++)sq.push_back((sth){i*p*r,i*p*r+vc});
vector<sth> s1=doit(sp,sq);
for(int i=0;i<s1.size();i++)
{
ll l1=s1[i].l,r1=s1[i].r;
if(l1)l1--,as=(as+1ll*mod*mod-1-min(l1%(p*q),va)-(va+1)*(l1/p/q))%mod;
as=(as+1+min(r1%(p*q),va)+(va+1)*(r1/p/q))%mod;
}
return 1ll*p*q*r%mod*as%mod;
}
int main()
{
scanf("%lld%lld%lld%lld%lld%lld",&v1,&l1,&v2,&l2,&v3,&l3);
l1+=v1;v1--;l2+=v2;v2--;l3+=v3;v3--;
printf("%d\n",solve(v1,l1,v2,l2,v3,l3));
}
AGC049D Convex Sequence
Problem
给定 ,求有多少个长度为 的非负整数序列 满足如下条件:
答案模 。
Sol
考虑如何描述一个凸序列。找到序列中的最小值,则这个位置向后斜率非负,且斜率一定不降。考虑斜率增加的量,如果在一个位置处斜率比之前增加了 ,可以发现这能够看成给从这个位置向后的每个位置依次加上 。(这也可以看成凸序列二阶差分非负,对这个二阶差分做两次前缀和的结果)
对于最小值向前的部分也可以类似操作。因此可以使用如下方式从全 序列 构造出当前的凸序列:
- 找到序列中第一个最小值的位置,设为 。
- 将 中所有数加上一个值,这个值等于 的最小值。
- 进行若干次操作,每次选择一个 满足 ,给 依次加上 。
- 进行若干次操作,每次选择一个 满足 ,给 依次加上 。同时为了满足 确实是最小值,需要至少做一次 的操作。
考虑这些操作对 的贡献,它们都只会增加值,第二步可以看成进行若干次操作,每次给所有数加一,因此和会增加 。在最后一种操作中,选择 会使和增加 ,上一种操作类似。
如果不考虑必须选一个某种操作的限制,则这个问题可以看成,有一些重量为 的物品,每种物品你可以选任意个,求有多少种方式选出重量总和为 的物品。显然这可以背包解决。
枚举 ,则第 种操作相当于重量为 的物品,第三种操作相当于重量为 的物品,同时必须选一个 。
考虑对于每个 求出只考虑重量为 的物品时的背包情况,用 的情况减去 的情况即可得到必须选 时的情况。
然后考虑加入 ,对于每个 求出只考虑重量为 的物品时的情况。两侧进行合并即可 得到一个 的答案。
注意到重量是平方量级的,因此所有部分只需要处理到 ,枚举 也只需要到 。因此复杂度为 。
注意到背包问题在算方案数时可以删除物品,因此这里也可以从小到大考虑 并加入删除物品。
Code
#include<cstdio>
using namespace std;
#define N 100500
#define M 455
#define mod 1000000007
int n,m,dp[M][N],f[N],ls[N],as;
int main()
{
scanf("%d%d",&n,&m);
int li=n-1;while(1ll*li*(li+1)/2>m)li--;
dp[0][0]=1;
for(int i=1;i<=li;i++)
{
int tp=i*(i+1)/2;
for(int j=0;j<=m;j++)dp[i][j]=dp[i-1][j];
for(int j=tp;j<=m;j++)dp[i][j]=(dp[i][j]+dp[i][j-tp])%mod;
}
for(int i=0;i<=m;i+=n)f[i]=1;
for(int i=0;i<=li;i++)
{
int l2=n-1-i;if(l2>li)l2=li;
for(int j=0;j<=m;j++)as=(as+1ll*(f[j]+mod-ls[j])*dp[l2][m-j])%mod,ls[j]=f[j];
int tp=(i+2)*(i+1)/2;
for(int j=tp;j<=m;j++)f[j]=(f[j]+f[j-tp])%mod;
}
printf("%d\n",as);
}
AGC049E Increment Decrement
Problem
给定正整数 ,定义一个长度为 的非负整数序列 的代价为如下问题的答案:
当前有一个长度为 的序列 ,初始所有 ,你可以进行如下两种操作:
- 将一个位置的 增加 或者减少 ,代价为 。
- 选择一个区间,将这个区间的 整体增加 或者整体减少 ,代价为 。
求使得 需要的最小代价。
现在给出 个长度为 的正整数序列 。对于每一个 ,从第 个序列 中选出一个元素作为 。可以发现这样能得到 种序列 ,求出这 种序列的代价的总和,答案模 。
Sol
考虑子问题的求解。首先单独考虑两种操作,如果只有第一种操作,则答案为 。如果只有第二种操作,考虑差分的变化不难发现答案为 。
然后考虑合并两种操作,设第二种操作对第 个位置的变化量为 ,则问题相当于找到一组 ,最小化 。
代价只和每个 以及每一对相邻的 相关。设 表示考虑了前 个位置,当前 时前面部分的最小代价。则转移为:
但这样初值处理略微麻烦(初值只有一个位置,不是凸函数),考虑换一种写法。设 表示考虑了前 个位置,当前 ,前面部分不考虑最后一个 的最小总代价。这样转移可以写成:
如果这样写,则可以发现初值为 且答案为 ,这两部分都容易处理。
转移中第二部分相当于将 和 做一个 卷积,第一部分是加上一个 ,可以发现这些都是上凸函数,由凸函数的形式,可以得到所有的 关于 都是上凸函数。
考虑经典维护凸函数的方式,即维护零位置的值 以及所有斜率改变的点构成的可重集 。初始 ,因此 且 中包含 个 。
考虑一次转移,加上一个 相当于给 加上 ,然后向 中加入两个 。
然后考虑后面的卷积,如果这次转移前斜率变化是从 到 ,则上一步后斜率变化是 到 。此时可以发现卷积一个两段斜率为 的函数相当于将当前凸函数的第一段和最后一段斜率分别变为 ,删去第一段可以看成删去 中最小的一个元素 ,然后让 减去 (可以发现 中元素全部非负),而删去最后一段相当于删去 中最大的元素。
因此问题可以使用如下方式求解:
初始 , 中包含 个 。依次考虑每个 ,进行如下操作:向 中加入两个 ,给 加上 。删去 中最小的元素,设其值为 ,给 减去 。最后删去 中最大的元素。完成整个过程后, 即为答案。
回到原问题,上述过程中加的部分的总量是一个定值,只需要考虑减的部分。考虑线性性,即对于每一种权值 ,统计所有情况中有多少次减去的值大于等于 ,显然容易由此得到答案。
对于一个 ,可以将小于 的元素看成 ,大于等于 的元素看成 ,这样之后相当于求有多少次取出最小值取出的是 。
此时 中只需要记录 的个数,因此设 表示考虑了前 次转移,当前 中有 个 的方案数,再记 表示这些情况中前面总共有多少次取最小值取出 ,dp
即可 得到答案。对于每一种权值做一次这个 dp
即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 53
#define mod 1000000007
int n,c,k,a,ci[N],dp[N][N*2][2],as,su;
pair<int,int> sr[N*N];
int main()
{
scanf("%d%d%d",&n,&c,&k);
for(int i=1;i<=n;i++)for(int j=1;j<=k;j++)scanf("%d",&a),sr[(i-1)*k+j]=make_pair(a,i),su=(su+a)%mod;
sort(sr+1,sr+n*k+1);
for(int t=1;t<=n*k;t++)
{
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j][0]=dp[i][j][1]=0;
dp[0][c][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=c;j++)
for(int t=0;t<2;t++)
{
int vl=t?ci[i+1]:k-ci[i+1];
int f1=1ll*dp[i][j][0]*vl%mod,f2=1ll*dp[i][j][1]*vl%mod;
int nt=j+t*2;
if(nt)nt--;else f2=(f2+f1)%mod;
if(nt>c)nt=c;
dp[i+1][nt][0]=(dp[i+1][nt][0]+f1)%mod;
dp[i+1][nt][1]=(dp[i+1][nt][1]+f2)%mod;
}
for(int i=0;i<=n;i++)as=(as+1ll*dp[n][i][1]*(sr[t].first-sr[t-1].first))%mod;
ci[sr[t].second]++;
}
for(int i=1;i<n;i++)su=1ll*su*k%mod;
printf("%d\n",(su+mod-as)%mod);
}
AGC048D Pocky Game
Problem
有 堆石子排成一列,初始第 堆石子有 个。
两个人进行博弈。两人轮流操作,第一个人先手。
第一个人操作时,他可以从当前左侧第一堆非空的石子中拿走任意数量石子,但至少需要拿走一个石子。
第二个人操作时,他可以从当前右侧第一堆非空的石子中拿走任意数量石子,同样至少需要拿走一个石子。
不能操作的人输,求双方最优操作下谁获胜。
多组数据,
Sol
考虑往最左侧的一堆加入一个石子,可以发现如果之前先手必胜,则加入后仍然先手必胜。这是因为如果之前先手必胜,则先手可以直接第一次操作多拿一个石子,变为之前的情况。
同时可以发现,如果最左侧的石子足够多,则先手可以选择每次拿一个石子,当后手拿完第二堆及之后的所有棋子时先手拿完所有石子获胜。因此有如下性质:
对于任意一个局面,存在一个正整数 ,满足如果可以任意改变第一堆石子的数量(但不能删完),则第一个人获胜当且仅当第一堆石子的数量大于等于 。
因此考虑设 表示当前剩下 中的所有石子,左侧额外有一堆数量为正但不确定的石子且第一个人先手时,左侧的石子至少需要有多少个才能使得第一个人获胜。
类似的,设 表示当前剩下 中的所有石子,右侧额外有一堆数量为正但不确定的石子且第二个人先手时,右侧的石子至少需要有多少个才能使得第二个人获胜。
考虑 的转移,为如下情况:
当前左侧的第一堆有 个石子,右侧的第一堆有 个石子。两人每次从对应方向的一堆取任意数量石子。考虑一个人取完石子时的情况:
如果第一个人取完了左侧的石子,则接下来情况变为 ,此时第一个人获胜当且仅当此时右侧的一堆剩余石子数量少于 。
如果第二个人取完了右侧的石子,则情况变为 ,此时第一个人获胜当且仅当左侧第一堆剩余石子数量大于等于 。
如果 ,则第一个人只要第一次操作取完自己的一堆就能获胜,此时 。
否则,先手不能直接获胜,接下来后手可以进行一次操作,因此先手的石子数量不能小于等于 。这时问题可以看成两堆石子分别有一个下界(),初始两堆石子数量都大于等于下界,如果一个人操作使得自己的这堆石子数量小于了下界,则下一次操作的人可以直接获胜。(如果取完了则对手直接获胜,如果没取完则对手下次直接取完即可获胜)。因此双方的策略都是尽量不少于下界,从而双方每次都只会拿一个石子直到有人小于下界,可以发现先手获胜当且仅当 ,即 .
类似地, 可以由 得到。可以区间 dp
求出所有的 ,最后第一个人获胜当且仅当 。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 105
#define ll long long
ll df[N][N],dg[N][N];
int T,n,v[N];
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]),df[i][i]=dg[i][i]=v[i]+1;
for(int i=1;i<n;i++)
for(int l=1;l+i<=n;l++)
{
int r=l+i;
if(dg[l][r-1]>v[r])df[l][r]=1;
else df[l][r]=df[l][r-1]+v[r]-dg[l][r-1]+1;
if(df[l+1][r]>v[l])dg[l][r]=1;
else dg[l][r]=dg[l+1][r]+v[l]-df[l+1][r]+1;
}
printf("%s\n",df[2][n]<=v[1]?"First":"Second");
}
}
AGC048E Strange Relation
Problem
给定正整数 ,对于一个长度为 的序列 ,定义序列 为如下问题的答案:
称长度为 的序列 对于序列 是好的,当且仅当 满足如下条件:
,满足 且 的 数量为 。
定义为所有对于序列 是好的序列中字典序最大的序列。可以证明一定存在这样的序列。
现在给出 个长度为 的正整数序列 。对于每一个 ,从第 个序列 中选出一个元素作为 。可以发现这样能得到 种序列 。
对于每个 ,求出这 个 对应的 的第 项之和。答案模 。
Sol
首先考虑求 的问题。考虑一个 ,问题相当于有 个元素 ,其中 。需要选择一个 使得 中正好有 个元素小于 。
记 为 减去 中小于 的元素,则问题相当于找到一个 使得 。考虑 从小到大每次加一的过程,每次加一 最多增加 。注意到初始时 ,结束时 ,结合每次 最多增加 可以发现一定存在一个时刻 。
因此无论前面的 如何选择,一定可以找到一个 满足条件。因此求字典序最大的好的序列(即 )只需要依次考虑每一位,每次让当前的 最大即可。
但这个过程无法直接 dp
,因为需要记录之前的所有 。考虑换一种求的方式。考虑初始时让所有的 ,令 ,考虑此时最大的一个 (有多个取下标最小的),设下标为 ,则让 一定合法(左侧无论怎么取都不能使得 比 大),因此在字典序最大的方案中一定有 。此时对于所有的 ,一定有 ,即对于所有 , 都不满足条件,因此需要让后面的所有 减一,即 减去 。然后考虑忽略位置 继续这个过程,可以发现位置 不会影响之后的过程。因此可以得到如下求 的方式:
让所有 ,令 。进行如下过程 次:
选择当前没有被选择过的位置中 最大的位置 。对于所有满足 且 没有被选择过的位置 ,将 减一, 对应减 。
这种方式仍然无法直接 dp
。但这里只需要对于每个 求答案,即对于每个 求它在所有情况中总共会被减多少次。
考虑选择 时能否影响到 的问题。首先考虑相邻的两个位置 。可以发现在选择到这两个数中的一个之前,任意一个选择其它数的操作不会影响 的大小关系(一定同时减或者同时不减)。从而选择 时能影响到 当且仅当初始时 。
考虑更一般的情况,此时选择 中间的其它数也会让 的差发生变化。可以发现如下结论:
考虑初始的 ,对于每一个 ,如果选择到 时会影响 ,则让 减一。进行了这些操作后,如果当前 ,则选择到 时会影响 ,否则不会影响。
证明:考虑 间的问题时,可以只保留序列中 间的部分(操作其余部分不改变中间部分相对大小关系)。考虑使用上一部分的方式,如果只考虑 时可以将 减小到小于等于 ,即一定是先将 减小到这么多再选择 ,则考虑 时,选择 时 的值一定小于等于初始的 (只加入了选择 的操作,但选择这个不影响后面的相对大小顺序且只会减小 ),因此一定先选择 ,从而选择 时会影响 。如果不满足,则可以发现 不会小于等于 , 不可能比 先选。从而结论成立。
从而可以通过从 向前依次考虑每一位的方式得到最后的 。具体来说,初始令 。依次考虑 ,如果 ,则让 减一, 减去 。这样得到的 即为最后的 。
此时判定条件只用到了前面部分初始的 ,因此可以进行 dp
。枚举一个位置 以及这个位置上的数,考虑这种情况的贡献。设 表示当前考虑到第 个位置,有多少种选择 间元素的方案数满足当前 被减了 次。枚举下一个位置填的值即可直接转移,这样从 到 的复杂度为 。注意到转移时相当于需要求这个位置上可以填的数中有多少个 满足 ,即对于每个 求出这些数中有多少个大于等于 。这可以预处理前缀和,从而一次转移复杂度可以做到 。直接对所有的情况(一个位置以及它填的数)分别做一次 dp
即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 55
#define mod 1000000007
int n,m,k,v[N][N],as[N],dp[N],cr[N];
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)
{
scanf("%d",&v[i][j]);v[i][j]+=i*k;
for(int l=0;l<=i;l++)dp[l]=0;
dp[0]=1;
for(int l=i-1;l>=1;l--)
{
int li=i-l-1,su=0;
for(int p=0;p<=li;p++)cr[p]=0;
for(int p=1;p<=m;p++)
{
int vl=(v[i][j]-v[l][p]+k-1)/k-1;
if(vl<0)continue;
if(vl>li)su++;
else cr[vl]++;
}
for(int p=li;p>=0;p--)su+=cr[p],dp[p+1]=(dp[p+1]+1ll*dp[p]*(m-su))%mod,dp[p]=1ll*dp[p]*su%mod;
}
int si=0;
for(int l=0;l<i;l++)si=(si+1ll*(i-l-1)*dp[l])%mod;
for(int l=i+1;l<=n;l++)si=1ll*si*m%mod;
as[i]=(as[i]+si)%mod;
}
for(int i=1;i<=n;i++)printf("%d\n",as[i]);
}
AGC048F 01 Record
Problem
考虑如下操作:
当前有一些正整数 。你需要重复进行如下操作,直到所有数变为 :
选择一个 的数 ,写下一个数字 ,然后将 减一。
称所有写下的数字按照顺序得到的 串为 。
现在给一个长度为 的 串 ,求有多少种不同的可重集 ,满足如果上述问题中正整数组成 ,则上述操作可以得到串 。答案模 。
Sol
考虑 对应的操作。依次考虑每一位,则 相当于选择一个奇数将其减一, 相当于选择一个偶数将其减一(只能选正数),一个 合法当且仅当 可以通过这些操作变为全 。
此时有如下结论:一个 合法当且仅当可以通过每次操作选择最大的能选择的数变为全 。
证明:如果存在一个合法但没有每次选择最大的能选数的操作方式,找到第一个没有选择最大的数的操作,考虑通过调整之后的操作使得这次操作选择最大的数且合法,如果能完成调整,则依次调整即可得到每一步都选择最大的数的方案。
考虑一步上的情况,设当前操作的数为 ,最大的数为 。考虑只调整涉及到这两个数的操作,使得这一步选择 。将这次操作及之后这两个数的操作单独拿出来构成一个序列,则这个序列可以看成对 进行题目中的操作得到的一个串。而调整使得这一步选择 相当于此时第一步选择 。即只需要证明任意一个得到的串都可以在第一步操作中选择 而得到。
考虑 均为偶数的情况,此时操作一个元素得到的为序列 0101...01
,操作两个数相当于将两个序列归并,从括号序角度容易发现归并后得到的串由若干 01
或 00101...01011
拼接而成。
每一段的 数量相同,因此每一段结束后两个数都是偶数。因此一段 01
一定是让一个数减二。考虑另外一种形如 00101...01011
的串,其中中间有 个 01
。此时显然两个数都需要至少减二,而两个数总共需要减 。可以发现对于任意的 ,一定可以让第一个 选择的数减去 ,让另外一个数减去 。具体来说,考虑中间长度 的 0101...01
段,将前 的 01...0
以及最后一个 1
给第二个数,将后面的 10...1
以及第一个 0
给第一个数即可得到一种构造方式。
如果一种方式使得整个序列第一个 0
选择了较小的 ,考虑这个 0
所在的段,如果这一段不是 01
则可以直接调整这一段使得第一个位置选择 。否则,考虑让这段 01
选 ,只需要证明 时,如果之后部分能被 操作得到,则之后部分能被 操作得到。如果之后存在一个 01
减小了 ,则将它变为减小 即可从 变为 。否则,因为 ,一定存在一个 001...011
的段,这一段中 减少的数量大于 减小的数量。由上述分析这里一定可以调整使得 少减少 , 多减少 ,这样就完成了调整。
考虑 为奇数的情况,此时序列中有两个 1
分别作为第一次选择 的位置,剩余部分和之前一样。可以发现第一个 1
对应的数必须在开头的一些 01
中全部被选,直到第二个 1
出现。设这一部分总共减去了 ,则改变开头选择的数相当于需要证明如果后面部分能被 操作得到,则可以被 操作得到。可以发现上一部分证明了 的情况,这里连续使用 的结论即可证明。
这样就证明了 的情况,由之前的调整即可证明一般情况下的结论。
回到原问题,考虑 合法的条件。首先容易得到如下两个限制:
- 中元素之和为 。
- 考虑每个时刻 中奇数偶数的个数以及操作,可以发现初始时 中奇数的数量必须为一个定值,偶数的数量不能小于某个值。
这两个限制保证了一定可以进行操作且操作正好进行 次,只需要再考虑能否让所有数变为 。
考虑能否让一些数变为 ,可以发现如下结论:
定义 为只考虑 中的 个偶数和 个奇数,操作最多能使这些数减去多少(即能操作这些数就操作这些数,不考虑负数的问题)。具体来说,可以使用如下方式计算 :记录这些数中当前偶数和奇数的个数 ,依次考虑每个字符,如果当前字符为 0
,此时如果 ,则让 减一, 加一, 的值加一,否则不动,1
的情况类似。可以证明这样贪心操作能减这些数的次数最多(如果有一次能操作而不操作,则将这个数的下次操作移到这次操作不会变差,从前往后依次调整即可得到结论)。
一个 合法,当且仅当对于任意一个 ,设 中大于等于 的元素中有 个偶数, 个奇数且和为 ,则 。
证明:如果存在合法的操作方式,则对于每一个 ,所有初始大于等于 的元素会被减小 次。而上述 为对这些数最多能操作的次数,因此一定有 。
接下来证明必要性。如果一个 不合法,则使用每次选择最大的能操作的数的方式操作后,最后 中有一些正数有一些负数。
将 中元素按照值从小到大依次编号,将这些数称为 。钦定在上述过程中选最大的数时,如果有多个相同的数则选择编号最小的。此时不难发现在任意时刻,随着编号增加对应元素的值单调不降。这样最后的正数为编号的一段后缀,即初始权值前 大的数。
注意到如下结论:如果两个数当前值的差小于等于 ,使用之前的贪心方式进行操作时,这两个数任意时刻值相差不超过 。
证明:每一步差距最多增加 。且如果差距为 ,则能操作这两个数时一定操作更大的一个。
设最后为正数的数中,初始权值最小的数初始权值为 。考虑奇偶性可以发现最后所有数一定是偶数,因此这个数最后权值大于等于 。设它的下标为 。
记录一个下标的集合 ,初始 。考虑操作的过程。如果在某一步操作时,有一些数和 当前的权值相同,则将这些数中小于 的下标都加入 。重复考虑所有操作直到没有元素能被加入 。
考虑此时 的性质。有如下性质:设 中最小元素为 ,则从 被加入时选择的操作开始到结束都有 。
证明:显然加入 的顺序为从大到小,考虑对 从大到小归纳。如果加入了一个数 ,则意味着某个操作前 。由归纳假设,在另外一个操作及之后都有 。如果后者在前者之前则显然得证。否则有两种情况:
- 在这两次操作间没有操作 ,此时到第二个操作时仍然有 ,因此此时满足 ,再使用之前的结论即可得到之后满足这一条件。
- 在这两次操作间操作了 。因为操作只会选择奇偶性相同的数中最大的一个,因此此时最大的数不超过 ,显然有 。
因此该性质得证,考虑最后 中最小的下标 ,这说明最后 。
而构造 的方式说明,不存在一个时刻使得 。因此在之前的贪心方式(选择奇偶性正确的数中值最大的,有多个选择编号最小的)中,如果能选择一个 间的数,则一定不会选择编号更小的数(不会出现相等情况)。
这表明在贪心操作中会选择能操作 间的数就操作 间的数,但这样最后这些数都非负且有一些数为正。设这些数中有 个偶数, 个奇数且和为 ,则这说明 。
这样证明了不合法一定推出右侧不成立,因此必要性得证。
因此只需要验证这三个条件是否成立,而这个条件可以按照值从大到小加入数考虑。具体来说,设 表示只考虑大于等于 的数,当前加入的数和为 且有 个偶数, 个奇数时有多少种方式保证大于等于 的部分条件 成立。有两种转移:加入一个 或者变为考虑 。在第二种转移时同时判断是否满足条件。另外两个条件可以在最后验证。(事实上偶数个数的条件不需要验证,如果偶数数量不够在条件 中取 考虑所有数即可推出不合法)
注意到这个状态数是 的,转移是 的。而计算所有 也是 的,因此总复杂度 。
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 305
#define mod 1000000007
char s[N];
int n,cl,nw,sl[N][N],as=0;
int dp[N][N][N];
int main()
{
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)
{
if(s[i]=='1')nw++;else nw--;
if(cl<nw)cl=nw;
}
if(cl>nw){printf("0\n");return 0;}
for(int i=0;i<=n;i++)for(int j=0;i+j<=n;j++)
{
int v1=i,v2=j,su=0;
for(int k=1;k<=n;k++)if(s[k]=='1')su+=!!v1,v2+=!!v1,v1-=!!v1;
else v1+=!!v2,v2-=!!v2;
sl[i][j]=su;
}
dp[0][0][0]=1;
for(int i=n;i>=1;i--)
for(int j=0;j<=n;j++)
for(int k=0;k<=j/i;k++)
for(int l=0;k+l<=j/i;l++)
if(sl[k][l]<(j+k)/2)dp[j][k][l]=0;
else if(i+j<=n)dp[j+i][k+i%2][l+1-i%2]=(dp[j+i][k+i%2][l+1-i%2]+dp[j][k][l])%mod;
for(int i=0;i<=n;i++)as=(as+dp[n][cl][i])%mod;
printf("%d\n",as);
}
AGC047D Twin Binary Trees
Problem
有两棵高度为 的满二叉树,它们的节点使用如下方式标号:
根节点标号为 ,一个标号为 的节点的左儿子标号为 ,右儿子标号为 .
可以发现每棵树上有 个叶子,标号依次为 。称一棵树的第 个叶子为标号为 的节点。
现在给一个 阶的排列 ,表示对于每一个 ,第一棵树的第 个叶子和第二棵树的第 个叶子间有一条边。称这些边为关键边。
称当前图上一个简单环是好的,当且仅当这个环正好经过两条关键边。定义一个环的权值为经过的所有节点标号乘积,求所有好的简单环的权值和,模 。
Sol
枚举经过的两条关键边,可以发现题目要求的值相当于 ,其中 表示一棵二叉树上第 个叶子之间的路径上所有点标号的乘积。
再考虑拆开贡献,定义 表示 到根的路径上经过的点标号乘积,,则有 。因此在 中,除去只与 或只与 相关的贡献后只剩 ,即只与两棵树上的 LCA
相关。
考虑枚举第一棵树上的 LCA
,则问题形式变为对于一个点,你需要对于所有 为它左子树内的叶子, 为它右子树内的叶子的情况求和 。此时只剩一个 LCA
,因此可以虚树上dfs解决 注意到树深度很小,可以使用如下方式:首先对于每一个左子树内的叶子,将它在另外一棵树上对应的 到根的路径加上当前叶子权值。然后考虑每个右子树内的叶子,对于一个 ,考虑它到根的路径上每一个点不在路径上的儿子,这个儿子当前的值即为所有与 的 LCA
在这个点上的 权值和,这样即可计算贡献。最后撤销修改即可。从而一个子树内的问题可以 解决。
因为树是满二叉树,直接这样做的复杂度即为
Code
#include<cstdio>
using namespace std;
#define N 263001
#define mod 1000000007
int n,p[N],vl[N],su[N],as,ir[N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int solve(int l,int r)
{
int mid=(l+r)>>1;
for(int i=l;i<=mid;i++)
{
int nw=p[i]+(1<<n),v1=1ll*vl[i+(1<<n)]*vl[nw]%mod;
while(nw)su[nw]=(su[nw]+v1)%mod,nw>>=1;
}
int as=0;
for(int i=mid+1;i<=r;i++)
{
int nw=p[i]+(1<<n),v1=1ll*vl[i+(1<<n)]*vl[nw]%mod,sr=0,ls=0;
while(nw)sr=(1ll*sr*ir[nw]+mod+su[nw]-su[ls])%mod*ir[nw]%mod,ls=nw,nw>>=1;
as=(as+1ll*sr*v1)%mod;
}
for(int i=l;i<=mid;i++)
{
int nw=p[i]+(1<<n),v1=1ll*vl[i+(1<<n)]*vl[nw]%mod;
while(nw)su[nw]=(su[nw]+mod-v1)%mod,nw>>=1;
}
return as;
}
int main()
{
scanf("%d",&n);n--;
for(int i=0;i<1<<n;i++)scanf("%d",&p[i]),p[i]--;
vl[1]=1;for(int i=2;i<1<<n+1;i++)vl[i]=1ll*vl[i>>1]*i%mod;
for(int i=1;i<1<<n;i++)ir[i]=pw(i,mod-2);
for(int i=1;i<1<<n;i++)
{
int lb=i,rb=i;
while(lb<1<<n)lb*=2,rb=rb*2+1;
lb-=1<<n;rb-=1<<n;
int tp=1ll*i*solve(lb,rb)%mod,nw=i;
while(nw)tp=1ll*tp*ir[nw]%mod*ir[nw]%mod,nw>>=1;
as=(as+tp)%mod;
}
printf("%d\n",as);
}
AGC047E Product Simulation
Problem
有一个长度为 的数组 。你可以在数组上进行两种操作:
- 给定 ,令 。
- 给定 ,令 ,即如果 结果为 ,否则结果为 。
你需要构造一个长度不超过 的操作序列,使其能实现两个非负整数相乘。具体来说,对于任意的 ,你的操作序列需要满足如下条件:
初始时 ,剩余位置上全部是 ,在使用了你的操作序列后, 位置上的值为 。同时任意情况下数组中的值不超过 。
Sol
考虑实现一些基础操作。首先考虑如何得到一个 。考虑先将 存到某个位置 ,再用 来得到 ,其中 为某个空位置的值。此时如果 则这样不能得到 ,但可以发现 时任何操作结果都是 ,因此任何操作合法。因此可以忽略这种情况。在 的基础上,可以进行倍增()从而得到 的幂。
考虑将 拆成二进制,然后维护二进制下的乘法。接下来依次考虑每一步:
- 得到一个位置的值的二进制表示。
设当前数为 。一种方式是从大到小考虑每个 的幂,假设当前处理到 ,考虑询问 是否小于 ,如果 更高位都为 ,则这个结果即为 在这一位的值。然后考虑去掉 的这一位以继续之后的过程,可以将这个值复制出来()然后倍增 次,这样如果这一位是 现在这个值就是 ,否则这个值是 ,然后将 减去这个值即可继续这个过程。
但这里只能相加不能直接相减,因此额外考虑维护一个值 表示之前需要减的东西,每次询问改为询问 是否小于 ,然后将本来需要减的值加给 。这样即可在 次操作内得到一个位置的值的二进制表示。
- 进行二进制相乘。
考虑二进制相乘的方式,直接方式为枚举每一对 ,如果第一个数的 位为 ,第二个数的 位为 ,则给答案加上 。考虑直接实现这个方式,此时需要一种方式实现两个 变量的与操作,这可以使用如下方式实现:先将两个变量相加,然后考虑 是否小于相加的结果,显然此时结果为 当且仅当原来两个变量都为 。然后将这个结果倍增 次加给答案即可。
直接实现的操作步数为 ,这里也可以将最后一步的 次倍增合并到一起做,将操作步数变为 。
Code
#include<cstdio>
using namespace std;
#define N 200500
int n,ct,s[2][35],s0;
int as[N][4];
void doit(int op,int a,int b,int c){as[++ct][0]=op;as[ct][1]=a;as[ct][2]=b;as[ct][3]=c;}
int main()
{
doit(0,0,1,3);doit(1,4,3,4);s0=4;n=4;
doit(0,0,s0,0);doit(0,1,s0,1);
for(int t=0;t<2;t++)
{
int vl=++n;
for(int x=30;x>=0;x--)
{
++n;doit(0,s0,n,n);
for(int i=1;i<=x;i++)doit(0,n,n,n);
doit(0,n,vl,n+1);
doit(1,n+1,t,n+2);
s[t][x]=n+2;
doit(0,n+2,n+3,n+3);
for(int i=1;i<=x;i++)doit(0,n+3,n+3,n+3);
doit(0,vl,n+3,n+3);n+=3;vl=n;
}
}
for(int i=0;i<=30;i++)for(int j=0;j<=30;j++)
{
++n;doit(0,s[0][i],s[1][j],n);
doit(1,s0,n,n+1);
for(int k=1;k<=i+j;k++)doit(0,n+1,n+1,n+1);
doit(0,n+1,2,2);n++;
}
printf("%d\n",ct);
for(int i=1;i<=ct;i++)printf("%c %d %d %d\n","+<"[as[i][0]],as[i][1],as[i][2],as[i][3]);
}
AGC047F Rooks
Problem
有一个无限大的棋盘,棋盘上有 个车,第 个车的位置为 。保证不存在两个车能互相攻击(即在同一行或同一列)。
对于每个 ,求出如下问题的答案:
现在在初始情况下,将棋盘上第 个车换成一枚特殊棋子。特殊棋子可以使用如下方式移动:
- 移动到上下左右相邻的四个格子中的一个,不能移动到一个被当前棋盘上的车攻击的位置。
- 如果与当前格子对角相邻的四个格子中的某一个上有一个车,则你可以走到这个格子上并吃掉这个车(将车移出棋盘)。
你可以操控特殊棋子移动,你希望吃掉距离多的车,在此基础上你希望移动次数尽量少。求出在吃掉的车数量最多的情况下最小需要的步数。
Sol
将所有车按照横坐标排序,设所有出现的横坐标从小到大排序为 ,所有纵坐标排序为 ,这样可以得到一个排列 ,使得第 个车所在的位置为 。
只考虑横坐标的移动,可以发现你不能在不吃掉第 个车的情况下从 的部分移动到 的部分,也不能移动回来(这里认为从 开始的问题即为一开始吃掉了 )。因此按照横坐标排序后,每个时刻吃掉的车编号一定构成一段区间 。考虑 坐标可以发现此时 的 坐标也必须构成 的一段,即 构成一个 中的连续段。此时可以发现当前棋子在不吃掉其它车的情况下,能移动到的范围为 的矩形。
考虑接下来的操作,当前可以移动到的区域所在的行列上都没有其它车,因此吃掉下一个车只可能是移动到矩形的某个角上,然后向这个角的方向斜向吃掉一个车。那么有四种情况:
- 如果 ,则可以移动到左下角再向左下吃掉 。
- 如果 ,则可以移动到左上角再向左上吃掉 。
- 的两种情况类似。
那么可以发现,在吃掉了 的所有车后,当前棋子的位置一定在原来的第 个车或者第 个车位置上。这样可以得到一种 dp
状态:设 表示当前吃掉了 内的车,在 或者 的位置上的状态的一些值。但这个状态数不能接受。
继续考虑问题的性质。首先考虑只求从一个位置出发能吃掉的车的数量的最大值,不考虑步数。
考虑扩展的过程,有如下结论:
无论选择什么扩展顺序,只要一直扩展到不能扩展为止,最后都能得到同一个区间。
证明:考虑一个位置上有两种扩展方式的情况,此时一定是能同时扩展左下和右上,或者左上和右下。考虑第一种情况。
如果此时选择向 方向(即左下)扩展,则可以归纳地发现每次向左下扩展后,仍然有 ,从而下一次扩展 仍然只能向左下。考虑一直向左下直到不能继续,设此时扩展到的左边界为 。
类似地,考虑一直向右上直到不能继续,设此时扩展到的右边界为 。则可以发现这一段的排列 一定是从 向左到 每个位置减一,向右类似,即形如 。
此时可以发现,如果当前扩展到的位置 满足 ,则一定能向左扩展,如果满足 则一定能向右扩展。考虑一侧到了边界的情况,如果当前位置为 且 ,可以发现此时不能向左下扩展,可以向右上扩展,因此只能向右上扩展。因此无论如何从 开始的扩展必定会扩展到 ,然后才可能向另外两个方向扩展。另外一种情况的过程类似。
因此对于从一个 出发的问题,扩展方式不影响扩展的长度,因此不需要再考虑吃掉的车的个数的问题。考虑使用上述证明中的方式进行扩展(即每次向两个方向同时扩展),记录每次得到的 ,则可以发现任意扩展状态必须经过 ,因此考虑只对这些状态记录 表示到达这个状态需要的最小步数,这样只有 个状态。
然后考虑不同状态间的转移。可以发现它相当于一个子问题,但此时所有车的 坐标递增, 坐标也递增(递减的情况类似)。这样显然可以吃掉所有车,而需要求出吃掉所有车并停在第一个/最后一个车位置时需要的最小步数。
此时问题相当于从初始位置出发,每次可以走到左侧第一个没有被经过的车或者右侧第一个没有被经过的车。可以发现从第 个车到第 个车的需要的步数为 (最后一步可以斜向),而由单调性这等于 。最后的 可以看成每吃一个车减去一步,总共吃掉的车数量是定值,因此可以最后考虑这些 。只考虑绝对值部分,则这个代价可以看成一维形式,即可以看成数轴上有 个点,点的坐标为 。你需要从起始点开始,经过所有点最后停在最左侧或者最右侧。那么显然最优解为从起始点一直向右到最右侧,再一直向左或者反过来。那么在二维原问题上最优解也是这个形式。
因此从一个状态转移到下一个状态的每一步可以 ,从而一个起始点的问题可以 解决。
考虑求所有起始点答案的情况。考虑排列中一个满足相邻两个位置差为 的极长段。可以发现极长段内一点作为起始点时,第一次扩展会扩展到这个极长段,接下来的扩展全部相同。考虑对于这个极长段接下来的转移,求出 表示在第一个扩展到的段停留在左侧/右侧,当前最后一个扩展到的段停留在左侧/右侧时的中间部分最小步数。一次扩展的过程相当于求出了这一段内一个 的转移矩阵,然后将这两个矩阵做 的乘法即可。这样可以求出 表示如果第一次扩展时停在左侧/右侧时,之后的最小步数。那么对于这一段内每个起始点,只需要求出 表示第一次扩展停在左侧/右侧的最小步数,这个点出发的答案即为 。这样一个段可以 解决。
问题是段的个数可能很多,但此时可以发现如下结论:
对每一个极长的满足相邻两个位置差为 的段进行一次扩展,扩展出的总长度为 。
证明:显然扩展时一定会将一个极长段整体加入(加入一个合法则剩余部分都可以加入),因此可以将每个段整体考虑。
考虑一个方向的扩展,这里考虑向右。此时有如下结论:
如果从小于 的位置开始扩展能扩展到第 段,则从第 段开始无法进行扩展。
证明:不妨设扩展到第 段时为向右上,这样第 段一定递增。如果从第 段开始时第一步能向左扩展,则左侧一段也是递增且该段的值域正好在第 段值域之下,此时这两段可以合并,矛盾。因此从第 段出发只能向右。而值为第 段值域下界减一的元素在这一段左侧(从左侧出发可以向右上扩展到这一段),因此从第 段向右不能向右下,只能向右上。而如果可以向右上扩展,则说明下一段为递增且下一段值域下界为这一段值域上界加一,此时这两段又可以合并,矛盾。因此从这一段开始无法进行扩展。
考虑之前的过程,如果选择到一个无法扩展的段则忽略,否则考虑这次扩展到的区间。显然区间内不能包含其它可以扩展的段,否则和上一条矛盾。记录所有可以至少扩展一次的段,考虑一段被扩展到的次数,可以发现一段最多被扩展到两次:只可能从左右两侧第一个能扩展到的位置扩展到当前位置,否则矛盾。
因此每一段最多访问到 次,从而直接对每一段进行扩展,扩展出的总长度为 ,因此直接做即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 200500
#define ll long long
int n,x[N],y[N],lx[N],ly[N],p[N];
ll f[2][2],g[2][2],h[2][2],as[N];
int Abs(int x){return x>0?x:-x;}
int calc(int x,int y){return Abs(lx[x]-lx[y])+Abs(ly[p[x]]-ly[p[y]]);}
void solve(int l,int r)
{
if(l==r)return;
f[0][0]=f[1][1]=0;f[0][1]=f[1][0]=1e18;
int l1=l,r1=r;
int mn=min(p[l],p[r]),mx=max(p[l],p[r]);
while(1)
{
int l2=l1,r2=r1;
if(l1>1&&(p[l1-1]==mn-1||p[l1-1]==mx+1))
{
l2--;
while(l2>1&&Abs(p[l2-1]-p[l2])==1)l2--;
}
if(r1<n&&(p[r1+1]==mn-1||p[r1+1]==mx+1))
{
r2++;
while(r2<n&&Abs(p[r2+1]-p[r2])==1)r2++;
}
if(l2==l1&&r2==r1)
{
ll lv=min(f[0][0],f[0][1]),rv=min(f[1][0],f[1][1]);
for(int i=l;i<=r;i++)
{
int dl=calc(i,r)+calc(l,r)-(r-l);
int dr=calc(i,l)+calc(l,r)-(r-l);
as[i]=min(dl+lv,dr+rv);
}
return;
}
if(r1==r2)g[0][0]=calc(l1,l2)-(l1-l2);else g[0][0]=calc(l1,r2)+calc(r2,l2)-(l1-l2)-(r2-r1);
if(r1==r2)g[1][0]=calc(r1,l2)-(l1-l2);else g[1][0]=calc(r1,r2)+calc(r2,l2)-(l1-l2)-(r2-r1);
if(l1==l2)g[1][1]=calc(r1,r2)-(r2-r1);else g[1][1]=calc(r1,l2)+calc(l2,r2)-(l1-l2)-(r2-r1);
if(l1==l2)g[0][1]=calc(l1,r2)-(r2-r1);else g[0][1]=calc(l1,l2)+calc(l2,r2)-(l1-l2)-(r2-r1);
for(int i=0;i<2;i++)for(int j=0;j<2;j++)h[i][j]=1e18;
for(int i=0;i<2;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)h[i][j]=min(h[i][j],f[i][k]+g[k][j]);
for(int i=0;i<2;i++)for(int j=0;j<2;j++)f[i][j]=h[i][j];
l1=l2;r1=r2;
mn=min(mn,min(p[l2],p[r2]));
mx=max(mx,max(p[l2],p[r2]));
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]),lx[i]=x[i],ly[i]=y[i];
sort(lx+1,lx+n+1);sort(ly+1,ly+n+1);
for(int i=1;i<=n;i++)
{
x[i]=lower_bound(lx+1,lx+n+1,x[i])-lx;
y[i]=lower_bound(ly+1,ly+n+1,y[i])-ly;
p[x[i]]=y[i];
}
for(int i=1;i<=n;i++)
{
int nw=i;
while(nw<n&&Abs(p[nw+1]-p[nw])==1)nw++;
solve(i,nw);i=nw;
}
for(int i=1;i<=n;i++)printf("%lld\n",as[x[i]]);
}
AGC046D Secret Passage
Problem
给一个长度为 的 串 ,你可以对 进行若干次如下操作:
取出 的前两个字符,删去其中一个字符,然后将另外一个字符插入到任意位置。
求通过对 进行操作可以得到的不同字符串数量,答案模 。
Sol
考虑不将字符直接插入回去,而是保留需要被插入的字符,在需要时(这个字符再次在开头被取出来或者操作结束)再插入。
此时的状态可以通过原字符串剩余的后缀长度 以及当前保留的数中 的个数 和 的个数 表示,且此时操作有三种取数方式:
- 取原字符串剩余部分的开头两个字符。
- 取原字符串剩余部分开头的一个字符和一个保留字符。
- 取两个保留字符。
状态只有 个,而状态的转移数量为 ,因此可以从初始状态 开始,使用 dfs
的方式 得到所有可达的状态。
但一个最后的字符串可能多个状态都能表示,因此考虑判断一个串是否能被表示。可以发现一个串能被表示当且仅当存在一个 ,满足 中长度为 的后缀为当前串的子序列,且设当前串除去这个子序列外有 个 , 个 ,则状态 可以在上一步中达到。
对于一个串 ,考虑找到最长的 ,使得 中长度为 的后缀为 的子序列,记这个长度为 ,可以发现上述判断只需要一个串的 以及串中 的数量 。考虑如何求出所有的 是否能被表示出,枚举 ,然后考虑所有的 ,找到上一步中最小的合法 ,此时这种情况能被表示当且仅当 。这部分可以 求出。
然后考虑如何对于每组 计算有多少个字符串 满足它由 个 和 个 组成,且 为对应值。注意到求 可以贪心,即倒着考虑字符串每一位,如果当前字符和 上一个需要被匹配的字符相同就匹配并增加长度,否则不操作。考虑将这个过程结合 dp
,设 表示有多少个由 个 和 个 组成的字符串 满足 ,转移时向开头加入字符,显然可以贪心得到新的 。最后对于所有能表示的 求和 即可。
复杂度
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 312
#define mod 998244353
int n,dp[N][N][N],vis[N][N][N],as;
char s[N];
void dfs(int x,int y,int l)
{
if(vis[x][y][l])return;vis[x][y][l]=1;
if(l>=2)for(int t=0;t<2;t++)dfs(x+(s[l-t]=='0'),y+(s[l-t]=='1'),l-2);
if(l&&x)dfs(x,y,l-1),dfs(x-1+(s[l]=='0'),y+(s[l]=='1'),l-1);
if(l&&y)dfs(x,y,l-1),dfs(x+(s[l]=='0'),y-1+(s[l]=='1'),l-1);
if(x&&x+y>1)dfs(x-1,y,l);
if(y&&x+y>1)dfs(x,y-1,l);
}
int main()
{
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i*2<=n;i++)swap(s[i],s[n-i+1]);
dp[0][0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;i+j<=n;j++)for(int t=0;t<=i+j;t++)for(int p=0;p<2;p++)
dp[i+!p][j+p][t+(s[t+1]==p+'0')]=(dp[i+!p][j+p][t+(s[t+1]==p+'0')]+dp[i][j][t])%mod;
dfs(0,0,n);
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++)for(int k=0;k<=n;k++)if(vis[j][k][i])
{
int tp=s[i+1]-'0';
int nj=j-!tp,nk=k-tp;
if(nj>=0&&nk>=0)vis[nj][nk][i+1]=1;
}
for(int i=0;i<=n;i++)
{
int s1=0,s2=0;
for(int j=1;j<=i;j++)if(s[j]=='1')s2++;else s1++;
for(int j=0;j+s1<=n;j++)for(int k=0;k+s2<=n;k++)if(vis[j][k][i])
as=(as+dp[j+s1][k+s2][i])%mod;
}
printf("%d\n",as);
}
AGC046E Permutation Cover
Problem
给定 ,称一个只包含 间正整数的序列是好的,当且仅当它满足如下性质:
对于序列中任意一个元素,序列中存在一个包含它的子段,使得这个子段是一个 的排列。
现在给定 ,你需要构造一个好的序列,使得序列正好包含 个 , 个 ,……, 个 ,且序列是好的。
首先判断是否有解,如果有解则你需要找到字典序最小的方案。
Sol
首先考虑如何判断合法。只考虑两种值 ,考虑一个 ,显然存在一个子段包含它,包含一个 且不包含其它的 。则对于这个 ,一定存在一个方向,使得在序列上从 向这个方向走能在遇到下一个 之前遇到一个 。考虑只保留序列中的 组成一个序列,则这个条件相当于一个元素和与其相邻的元素不能全部相同,即不能有三个连续元素或者开头结尾的两个元素相同。
设两种元素出现次数为 。如果 ,则考虑将序列每三个分为一段,由上述结论可以发现一定有 。对于 的情况可以在开头结尾各分一个长度为 的段, 的情况可以在开头分一个长度为 的段,可以发现此时都有 。因此如果存在合法的方案,则一定有 。
另一方面,考虑满足 的情况,注意到对于任意一个 的子集 ,都可以构造一段合法的序列 (其中 为 的补集),使得这一段中 中元素使用了两次,其余元素使用了一次。因此考虑每次将 最大的元素使用两次,其余元素使用一次,可以发现 时这样一定能得到解。
因此有解的充分必要条件为 。
然后考虑确定最小字典序的问题。考虑逐位确定答案,此时需要确定固定了一个前缀后是否存在解。
考虑此时的情况,此时一定是前缀中的一段后缀还没有被覆盖到,需要后面部分覆盖。考虑找到已经填的部分中从后往前第一个元素重复出现的位置,则这个位置之前的部分不会和后面新加入的部分构成一段 的排列,只需要考虑之后的部分。因此当前的问题可以看成,已经填了一些数且它们两两不同,其中前面的一些数已经被覆盖过了,剩余数还需要被覆盖。
设已经被覆盖过的数依次为 ,没有被覆盖过的数依次为 。考虑什么情况下存在解。假设存在一组解,考虑找到从 开始第一个满足向后 个元素构成排列的位置 ,这些位置一定属于 中的一个。可以发现此时限制变为只考虑从 到结尾的这一段时,它能满足条件。即问题为在初始问题的基础上,当前开头填了一些数,没有一个数被用过两次,求是否存在解。
考虑一个简单情况,即开头已经将 间的每个数填了一次,得到了一个排列。考虑此时是否合法。可以发现如果 ,则不考虑开头的排列后面可以直接合法。而如果 ,则一定不可能合法。因此只需要再考虑 的情况。
设 ,则有一些数总共出现 次,另外一些数总共出现 次。考虑一种出现次数为 的数 和一种出现次数为 的数 ,像第一部分那样考虑只保留这两种数的序列。如果序列的第一个元素为 ,则剩余部分有 个 , 个 ,考虑将接下来的位置每三个分一段直到结尾,可以发现此时如果每一段中的三个数不全部相同,则到 用完时 至少还有两个,因此第一个元素为 时不存在合法解。因此在解中,只保留 时第一个元素为 ,即 先于 出现。
那么在这个问题中,这相当于限制在开头的排列中,所有满足 的 必须出现在所有 的 之前。而如果满足这一条件,考虑在排列后填上一段排列的前缀,使得前缀包含所有 的位置但不包含 的位置,可以发现这一段以及之前的排列内部可以覆盖这部分,此时后面部分最大的 剩余 ,最小的 一段为 ,因此后面部分也可以单调构造合法方案。
从而可以发现,在这一问题中,存在合法解当且仅当 ,或者 且在排列中出现次数最多的那些数都在出现次数最少的数之前。
考虑上一个问题,即开头只是排列的一部分。此时可以看成你可以任意填排列剩余部分(显然前 个元素需要构成排列),需要找到一种合法排列。可以发现如下结果:
- 如果 ,则一定合法。如果 则一定不行。
- 对于 的情况,如果没有填入排列的元素中不存在 的,则存在合法解当且仅当每一个满足 的元素满足它没有在开头部分出现或者它出现的位置在所有 的元素出现位置之后。
- 如果没有填入排列的元素中存在 的,则最优方式一定是将这些元素填在接下来的开头,因此存在合法解当且仅当每一个满足 的元素都没有在开头部分出现。
最后回到之前的问题,这里需要在 中找到一个开头,使得这个开头到 的部分满足上一个条件。即可以删除开头的一段前缀(但不能删到 ),求是否存在合法方式。
删一些数之后出现次数的 和 都可能减少 。因此可以发现 最多增加一。可以发现 时一定无解。而如果 ,则直接不删就能得到合法方案。只需再考虑剩余两种情况。
如果 ,此时删去开头时不能删去出现次数等于最小值的数,否则 再减一一定无解。那么只能在第一个出现次数等于 的元素以及 之前删去一些数,如果删去的数出现次数不等于 则显然不会造成影响,可以发现删次数等于 的数也不会改变合法性(如果删了它则它不会造成影响,但如果不删它,则它会作为一个 存在,但它左侧没有出现次数等于 的数,因此它也不会导致不合法)。从而可以发现此时如果合法,则不删开头也是合法的,因此在不删的情况下判断上一部分的条件即可。
如果 ,则一定需要将所有出现次数等于 的元素删一次,此时如果会删到次数等于 的或者删到 则无解。删去这些元素后,它们变为不在排列中出现,但它们仍然是最大值。此时为上一部分中没有填入排列的元素中存在 的情况,因此只有每一个 的元素都不在前面出现才有解。而它们也不能被删去,因此只能是它们在删去之前就没有出现。因此此时合法当且仅当如下两个条件得到满足:所有出现次数等于 的数都没有在 中出现且所有出现次数等于 的数都在 中出现。
从而可以 判定填了一段前缀的情况是否合法。因此可以逐位确定最小字典序答案时直接从小到大尝试每个数填入是否合法。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 105
int n,v[N],la[N],li,lc,ra[N],ri,rc;
int v2[N];
bool chk()
{
for(int i=1;i<=n;i++)v2[i]=v[i];
for(int i=1;i<=ri;i++)v2[ra[i]]++;
int mn=1e9,mx=0;
for(int i=1;i<=n;i++)mn=min(mn,v2[i]),mx=max(mx,v2[i]);
int s0=0,fg=0,s1=0,ci=0;
for(int i=1;i<=n;i++)ci+=v2[i]==mx;
for(int i=1;i<=ri;i++)
{
if(v2[ra[i]]==mn)s0=1;
if(s0&&v2[ra[i]]==mx)fg=1;
if(i>rc&&v2[ra[i]]==mx)s1=1;
if(v2[ra[i]]==mx)ci--;
}
if(mn*2>mx)return 1;
if(mn*2==mx)return !fg&&!(ci&&s0);
if(mn*2==mx-1)return !s0&&!s1&&!ci;
return 0;
}
int main()
{
scanf("%d",&n);
int tp=1e9,su=0;
for(int i=1;i<=n;i++)scanf("%d",&v[i]),tp=min(tp,v[i]),su+=v[i];
for(int i=1;i<=n;i++)if(v[i]>tp*2){printf("-1\n");return 0;}
while(su--)
for(int i=1;i<=n;i++)if(v[i])
{
int fg=1;
for(int j=1;j<=li;j++)ra[j]=la[j];
ri=li;rc=lc;
ra[++ri]=i;
for(int j=1;j<ri;j++)if(ra[j]==i)
{
if(j>rc){fg=0;break;}
for(int k=1;k<=ri-j;k++)ra[k]=ra[k+j];
ri-=j;rc-=j;break;
}
if(ri==n)rc=n;
if(!fg)continue;
v[i]--;
if(chk()){li=ri;lc=rc;for(int i=1;i<=li;i++)la[i]=ra[i];printf("%d ",i);break;}
else v[i]++;
}
}
AGC046F Forbidden Tournament
Problem
给定 ,求有多少个 个点的竞赛图满足如下条件:
- 图中每个点入度不超过 。
- 对于任意四个点 ,它们间的边方向不是 。
答案对给定模数 取模。
Sol
考虑竞赛图的性质,可以得到如下结论:
- 竞赛图缩强连通分量后一定得到一条链。
- 强连通竞赛图一定存在哈密顿回路。
这两条都是经典结论,这里只给出简要证明。对于第一条,可以考虑依次向竞赛图中加入点,维护当前缩点后的图,加入点时考虑当前缩点得到的链与加入点间边的关系,可以发现一定是向链中加入一个点或者将这个点和链上一段区间合并在一起。对于第二条,同样考虑在竞赛图中加入点,维护当前每一个强连通分量内的哈密顿回路。加入点时,如果这个点合并了多个强连通分量,则只需要在每个强连通分量内由哈密顿回路找一条固定起点或终点的哈密顿链,拼接起来即可得到新的强连通分量的哈密顿回路。如果这个点是加入一个强连通分量,考虑原先的哈密顿回路上每个点与加入点的边,可以发现一定存在回路上顺序相邻的两个点,使得第一个点连向加入点,加入点连向第二个点,因此可以将当前点插入哈密顿回路。
还有如下结论:
- 强连通竞赛图(除了单点情况)一定存在三元环。
证明:考虑一个点 ,设连向 的点集为 , 连向的点集为 ,则 都不是空集。此时如果不存在三元环,则 中一点 和 中一点 的边方向一定是 连向 。但这样 部分不强连通,矛盾。因此一定存在三元环。
考虑原问题中的限制,即不存在一个点以及另外的一个三元环,使得该点连向三元环的三个顶点。可以发现如下结论:
- 在强连通竞赛图中,不存在这样一个结构当且仅当将所有边反向后不存在这个结构,即当且仅当不存在四个点 使得边方向为 。
证明:只需要证明一个方向的结论,另外一个方向只需要将边全部反向即可得到。
考虑存在 的情况,此时需要证明存在另外四个点满足 。
考虑从 出发,沿着强连通竞赛图的一个哈密顿回路走,直到第一个满足向 至少连了一条边的点 。如果 向 都有边,则结论已经成立。
接下来考虑剩余情况。考虑 到 走过的路径,这条路径上除去 外,每个点 都满足图中 向 连边。因此这种情况下 中一定存在一个点,使得这个点向路径上所有点连边。因此如果路径经过的点中存在一个三元环,则这个三元环加上上一个点满足结论的条件。
否则,路径经过的点不存在三元环,只考虑这些点的导出子图时,每一个强连通分量都是单点,即对于这条链 ,边一定是从编号小的方向连向编号大的方向(即 )。因此存在 到 的边。
此时不妨设 连向了 ,则存在三元环 ,此时还有边 。因此如果 连向 ,则找到了满足条件的四个点 。否则,如果 连向 ,同理可以得到如果 连向 则存在满足条件的四个点,但如果同样是 连向 ,则 满足条件。因此一定存在满足条件的四个点,因此结论成立。
接下来开始考虑原问题。考虑一个满足条件的竞赛图,对这个图缩点后,如果除去最后一个强连通分量外有一个强连通分量大小大于 ,则它存在一个三元环,这个三元环和链上下一个强连通分量的某个点组成一个不合法的四元组。因此不存在这种情况,即图中除去最后一个强连通分量外每个强连通分量大小都是 。设前面的点为 ,则它们间的边满足如果 ,则有边 ,然后这些点向最后一个强连通分量连边。
因此考虑设 表示在这种 下有多少个强连通的竞赛图满足条件,则枚举前面的点数可以发现答案为:
考虑强连通的问题,此时可以使用上述关于强连通竞赛图的若干结论(包含结论 ),因此可以看成计算两种子图都不出现的方案数。
考虑一个点 ,考虑所有 连向的点,设这些点构成集合 。则 中不能出现三元环,否则三元环加上 导致不合法。因此由之前的结论,可以发现 中的所有点排列成一条链,且这条链满足边一定从链上前面的点连向后面的点。考虑再加入 ,设这条链为 。
现在考虑 ,重复上述讨论,可以发现 连向的点也构成一条链。注意到 一定连向了 ,因此当前的链也包含这些点作为一个子序列。考虑有一些新的点插入到 中或者之前的情况,即存在一个点 ,使得链上情况为 。可以发现此时有边 ,以及 。那么可以发现 组成一个三元环,它们都连向 ,这不合法。因此不会出现这种情况,即 之外新增加的点一定在链中加入在 之后。
从 到 依次考虑,可以发现每一步都可以使用上一个讨论,得到新加入的点排列在当前链末端。
然后考虑 的情况,可以发现 连向 。此时有如下结论: 连向当前还没有被加入的所有点。
证明:如果不满足这一条件,则这个点 连向 所有点。但 构成一个强连通分量,其中存在一个三元环,因此会导致矛盾。
考虑 连向的点形成的链,可以发现链中当前没有被加入的点和 之后的点一定在 前面。考虑将当前链中 前的部分加入,这样就加入了所有点,从而得到了一个环。这个环有如下性质:
对于环上任意一个点,它连向的点一定是环上这个点向后连续的一段。
证明:对于 部分,由构造过程的证明即可得到。同时可以发现这一段中随着下标增加, 能连到的编号最大的点的编号 也在增加,但不会超过 (因为 连向这一段中的所有点)。
考虑 部分,此时 连向剩余部分所有点,可以得到每个点都连向这一部分在自己之后的点。考虑连向另外一部分的边,由上述讨论可以发现 中连向这一部分的 的一定是编号的一段后缀。因此 连向的是这一段的一个前缀。由此可以得到 连向环上自己向后的一段。
上述过程对于每一个合法的强连通竞赛图构造了一个这样的环(哈密顿回路),接下来有如下性质:
- 如果一个强连通竞赛图中存在一个哈密顿回路满足对于环上任意一个点,它连向的点一定是环上这个点向后连续的一段,则它满足题目的限制。
证明:考虑图中的一个三元环 ,其中 。考虑另外一个点 ,不妨设 在 之间(另外两种情况等价)。由于 连向 ,考虑 可以发现 一定连向 ,考虑 可以发现 一定连向 。因此不可能 同时连向三个点或者三个点同时连向 。
- 对于任意一个强连通竞赛图,它最多有一个哈密顿回路满足上一个条件,即对于环上一个点,它连向的点一定是环上这个点向后连续的一段。
证明:考虑一个满足条件的回路 。如果存在另外一个回路 ,不妨设 ,考虑从 开始,找到第一个位置 满足 。那么这个回路会先经过 ,再经过某个点 ,再经过 。而图中有边 ,由回路的性质可得存在边 。但在第一个回路中考虑,可以发现不存在这样的 满足存在边 ( 连向的其余点一定满足 也连向它,否则矛盾)。因此不存在第二个回路。
因此,只有合法的图能找到这样的回路,且每个合法的图内正好存在一个这样的回路。
因此考虑计算回路为 时的合法竞赛图个数,乘以给环重新标号的环排列数 即为这种情况总的合法图数。(由上一条性质,可以发现不可能有多种标号方式或者不同回路得到同一个图)
设点 连向的点中编号最大的点为 ,则 都不会连回点 ,因此对于这些点存在一个 ,使得点 连向了 中的所有点。
那么此时可以得到一组 。其中 。考虑什么情况下这组 是合法的。
考虑 连出的边,由上述讨论和竞赛图性质可以发现它连向 中的所有点,且连向 中所有满足 的点 。
因此可以发现一个必要条件是 单调不降。同时可以发现如果满足这个条件则每个点连向的都是环上它之后的一段,因此这是必要条件。
因此由 ,以及 即可确定一个合法的回路,且这是一一对应。
现在考虑原问题的另外一个限制,即每个点的入度不超过 。
考虑 的入度以及环的限制,可以发现有限制
考虑 的入度,如果 ,则表示 的边都会连向 ,因此有 。同时可以发现如果 ,则连向 的边正好是 ,因此可以发现 是这部分合法的充分必要条件。
因此相当于需要统计有多少组 满足 单调不降,且 ,且 。对于每个 做一次即可得到 ,从而得到答案。
这里可以直接 前缀和优化 dp
,这样总复杂度为 。但还有更优的做法。
考虑把这个过程看成在网格上行走,每一段看成从 出发走到 ,再走到 。
那么问题相当于从 走到 ,只能向右向上,且不能经过 和 (这里因为会经过 所以需要特殊处理,但还需要额外特殊处理 的范围),求走的方案数。
这样还需要枚举 ,但考虑走到 后一直向上到上界,则相当于从 走到 且不经过这两条线的方案数。
考虑做一些坐标平移,此时问题可以看成,你要从原点向右向上走到 ,不能经过直线 ,求方案数。
考虑顺时针旋转 度,问题变为你可以向右上或右下走,走到 ,不能经过直线 ,求方案数。
如果只有一条直线(例如 ),则可以考虑减去不合法的方案数,如果一个方案经过了 ,考虑将方案在第一次经过 后的部分沿着 翻折,这对应一种走到将终点沿着 翻折到的位置的方案。而可以发现从起点到这个位置一定经过 ,因此对于一个这样的方案在第一次经过 的位置翻折回去,就得到了所以不合法方案和这种走的方案的一一对应。因此这时不合法的方案数位走到对应位置的方案数,这是一个组合数。
对于两条折线的情况,考虑分别减去经过了某条直线的方案数,但这样会将两条直线都经过的方案减两次。
因此考虑加上先经过了 ,再经过了 的方案数。统计这种方案可以先在经过 时做一次翻折(同时也会翻折 这条线),然后再在经过 翻折后的位置时做一次翻折,这样相当于向上翻折两次。可以发现这样翻折得到的方案数可以通过两次翻折回去与先经过 再经过 的方案对应。同样可以加上先经过 再经过 的方案数。
但这样又会将经过了 的路径多加一次,考虑一直做下去,可以发现用总方案数减去翻折一次的,加上翻折两次的,减去翻折三次的,加上四次的,以此类推,这样即可统计不经过两条直线的方案数。
可以发现每次都是向一个方向翻折,因此每两次翻折终点位置一定向上 ,从而只会翻折 次,预处理组合数后单次复杂度 。这种方式也被称为“多次翻折”。
考虑用这种方式解决原问题,可以发现在一个 的问题中,第一步枚举的 数量为翻折中的 ,而翻折复杂度为 ,因此单个 的问题可以 求出。从而可以在 复杂度内解决原问题。注意需要特判子问题中 的情况,此时回路可能出现细节问题。
Code
#include<cstdio>
using namespace std;
#define N 205
int n,k,p,c[N][N],as;
int calc(int x,int y)
{
if(y<-x||y>x)return 0;
y+=x;if(y&1)return 0;
return c[x][y/2];
}
int calcr(int x,int y,int l,int r)
{
if(y<=l||y>=r)return 0;
int as=calc(x,y);
int tp=r*2-y,fg=p-1,t1=r;
while(tp<=x)as=(as+1ll*fg*calc(x,tp))%p,fg=p-fg,t1+=r-l,tp=t1*2-tp;
tp=l*2-y,fg=p-1;t1=l;
while(tp>=-x)as=(as+1ll*fg*calc(x,tp))%p,fg=p-fg,t1-=r-l,tp=t1*2-tp;
return as;
}
int main()
{
scanf("%d%d%d",&n,&k,&p);
for(int i=0;i<=n;i++)c[i][0]=c[i][i]=1;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%p;
for(int i=0;i<n;i++)
{
int su=0,tp=n-i;
if(tp==1)su=k==n-1;
else if(tp==2)continue;
else
{
int rb=k-i,lb=tp-1-(k-i);
if(lb<1)lb=1;
if(rb>tp-2)rb=tp-2;
if(lb>rb&&tp>1)continue;
for(int j=lb;j<=rb;j++)
{
int rx=j+2,ry=1+j+k-i;if(ry>tp)ry=tp;
ry-=j+1;rx-=2;
su=(su+calcr(rx+ry,ry-rx,lb-j-2,rb-j+1))%p;
}
}
for(int j=1;j<=n;j++)if(j!=tp)su=1ll*j*su%p;
as=(as+su)%p;
}
printf("%d\n",as);
}
AGC045B 01 Unbalanced
Problem
定义一个只包含 的字符串 的不平衡度为:对于 的所有子串,子串中 出现次数之差的最大值。
给一个长度为 且只包含 01?
的字符串,你需要将每个 ?
替换为 中的一个。求替换后得到的字符串的不平衡度的最小值。
Sol
考虑将 看成 ,则不平衡度相当于子段和绝对值的最小值。
考虑答案的下界,对于一个区间,设将 ?
全部换为 后和为 ,则这一段的子段和不小于 ,如果全部换为 后和为 ,则子段和不小于 。将所有 ?
全部换为 分别求和最大/最小的子段和即可求出这个下界。设这个下界为 。
可以发现这个下界不一定是答案。例如全是 ?
的情况(下界为 )或者 0??0
(下界为 但答案是 )。
但可以发现如下结论:答案一定不超过 。
证明:如果最优解的不平衡度大于等于 。则相当于看成 后的序列前缀和中最大最小元素差大于等于 。考虑从开头开始向后找到第一个等于 或等于 的位置,不妨设找到的为 ,则考虑通过一些构造将 加一。
考虑一个前缀和等于 的位置 ,从这个位置出发向两侧找到第一个前缀和大于等于 的位置 。考虑 到 的这一段,这一段中一定有一个 ?
选择了 ,否则这一段会使得下界为 。同理, 到 的这一段一定存在一个 ?
选择了 。那么将这两个选择同时改变,则中间的前缀和整体 ,中间部分前缀和都小于 ,因此整体 后不改变 ,而这一段内 增加了至少 。考虑依次对当前存在的 做一次,则可以在 不变的情况下让 加一,从而不平衡度减一。(可能 不存在,此时只改变 即可)
一直进行调整,即可得到答案不超过 。
因此只需要判断答案是否能是 。如果答案为 ,则存在一个 ,使得所有前缀和都在 之间。考虑一个达到上界的段 ,不妨设这一段是全部 ?
看成 后和为 ,此时显然有 位置的前缀和为 , 位置的前缀和为 。因此可以得到 。那么在两种情况中考虑所有是一个达到上界的段的端点,每一个端点限制了 的奇偶性。可以发现奇偶性无法匹配时答案一定不可能是 (例如 1??1
的情况,此时 都是可能的左端点,但奇偶性不匹配)。
另一方面,可以发现如果奇偶性匹配,则一定可以构造出解。
首先将达到上界的段中的值全部确定,每一段都要求这一段内所有 ?
填 或 。此时不会出现不合法的情况(一个 ?
同时被要求填两种字符),因为如果出现这种情况,考虑经过它的两种不同大小关系()的区间,此时一定可以找到两个端点使得它们能导致更大的下界。且如果有多个段重叠,可以发现它们间奇偶性没有问题,从而一定可以构造出解。
最后考虑两个段中间的部分,此时这部分的奇偶性正确,因此一定可以找到一个方案连接两侧,但此时可能中间部分前缀和超过了限制。但注意到对中间部分单独考虑上界的结果小于等于 ,因此这一部分一定可以使用类似于上述证明中的调整方式满足限制。这里省略具体的调整细节。
因此只需要再判断依次奇偶性即可,可以对两种情况分别扫一遍找到达到上界的段的端点,最后判断奇偶性。
复杂度
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1000600
int n,s0,s1,sv;
char s[N];
void solve0(int t)
{
int mx=0,nw=0;
for(int i=1;i<=n;i++)
{
if(s[i]==t+'0')nw--;else nw++;
sv=max(sv,mx-nw);
if(mx<nw)mx=nw;
}
}
void solve1(int t)
{
int mx=0,nw=0;
for(int i=1;i<=n;i++)
{
if(s[i]==t+'0')nw--;else nw++;
if((i+sv*t)&1)s0=max(s0,mx-nw);else s1=max(s1,mx-nw);
if(mx<nw)mx=nw;
}
}
int main()
{
scanf("%s",s+1);n=strlen(s+1);
solve0(0);solve0(1);
solve1(0);solve1(1);
if(s0<s1)swap(s0,s1);
printf("%d\n",s0+(s0==s1));
}
AGC045C Range Set
Problem
有一个长度为 的字符串,初始时所有字符为 0
。给定 ,你可以进行如下两种操作:
- 选择 个连续位置将它们变成
0
。 - 选择 个连续位置将它们变成
1
。
求可以得到的不同字符串种类数,答案模 。
Sol
考虑判断一个串能否被表示。倒过来考虑操作过程,可以发现这相当于如下问题:
对于结束串 ,你可以进行如下两种操作:
- 选择 个连续位置,使得它们中不包含
1
,然后将它们全部变为?
。 - 选择 个连续位置,使得它们中不包含
0
,然后将它们全部变为?
。
能被表示当且仅当能将串操作到不包含 1
。
可以发现如果不包含 1
,则再进行一次覆盖整体的第一种操作则串全部变为 ?
。因此可以将合法条件变为能否将所有字符变为 ?
。因此 的顺序是不重要的,从而 的顺序也是不重要的,因此不妨设 。
考虑操作的过程,显然变为 ?
不会变差,因此一定是能操作就操作。同时,如果进行了一次操作 ,则当前存在 个连续的 ?
,可以发现此时一定可以将这一段左侧或者右侧的单个字符变为 ?
(通过操作这个字符和这一段的一部分),从而达到目标。因此只要某一步能进行操作 该方案一定合法。
考虑不合法的串,该方案一定串如下性质:
如果一个极长连续 0
段长度大于等于 ,则将它全部变成 ,否则不动。对所有这样的段进行操作后,不存在一个长度大于等于 的极长 段。
这个条件和极长全 段有关,考虑设 表示长度为 的 串,满足最后一个字符为 且前面进行操作后不存在长度大于等于 的极长 段,且最后一个 的位置为 的方案数。
考虑向后转移,枚举加入了 个 和一个 ,则转移有两种:
可以发现转移到的区间是固定第二维后第一维上的一段区间,因此可以前缀和优化转移。最后容易由 得到答案(考虑最后的一段 即可)
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5050
#define mod 1000000007
int n,a,b,fg,dp[N][N],as,su[N][N];
int main()
{
scanf("%d%d%d",&n,&a,&b);
if(a>b)a^=b^=a^=b,fg=1;
dp[0][0]=1;su[0][0]=1;
for(int i=1;i<=n;i++)
{
int lb=i-a;if(lb<0)lb=0;
for(int j=0;j<i;j++)
{
int s1=(su[i-1][j]+mod-(lb?su[lb-1][j]:0))%mod,s2=lb?su[lb-1][j]:0;
s1=(s1+mod-dp[i-1][j])%mod,s2=(s2+dp[i-1][j])%mod;
dp[i][i-1]=(dp[i][i-1]+s1)%mod;
if(i-j<b)dp[i][j]=(dp[i][j]+s2)%mod;
}
for(int j=0;j<=i;j++)su[i][j]=(dp[i][j]+su[i-1][j])%mod;
}
as=1;for(int i=1;i<=n;i++)as=2*as%mod;
for(int i=0;i<=n;i++)for(int j=0;j<=i;j++)if(dp[i][j]&&(n-i<a||n-j<b))as=(as+mod-dp[i][j])%mod;
printf("%d\n",as);
}
AGC045D Lamps and Buttons
Problem
有 盏灯,初始前 盏是亮的。
现在随机生成一个 阶排列 ,但你不知道这个排列。你可以进行如下操作:
选择一盏当前亮着的灯 。随后灯 的状态会改变。你可以知道操作后所有灯的状态。
如果所有灯都亮了则你获胜,如果不可能达到这一点则你失败。
你会选择一种策略最大化你获胜的概率。求你获胜的概率乘以 的结果,答案模 。
Sol
可以看成将所有灯连成了若干个有向环(即置换),选择一个点时会改变环上下一个点的状态。
可以发现如果一个环不是单点自环,则你可以操作环上一个点得到它指向的点,然后可以将这个点点亮,再从下一个点开始继续找接下来的点直到将这个环找完。这样就可以确定这个环上的所有点并将它们点亮。
但如果这个环是单点环(),则操作到 会立刻失败。
因此你的操作可以看成,每次选择一个没有选择过的亮的灯,如果它所在的环不是单点环则你会将它所在的环全部访问并点亮,如果是单点环则你直接失败。(显然你只能访问到选择过的灯所在的环,因此这样的操作是必要的)
由于 随机生成,因此选择一个没有选择过的灯时,不同的选择间等价。因此一种最优操作方式是每次选择编号最小的没有被访问过的点,进行上述操作。可以发现问题答案即为在这一确定的操作方式下能获胜的排列 数量。
考虑枚举前 个位置中第一个自环出现的位置 (不存在时认为 ),则能获胜当且仅当能在前 盏灯的操作后结束整个过程,即前 个点所在的那些环必须包含 中的所有点。
此时可以发现,满足这一条件的方案数相当于如下问题的答案:
给定 ,求有多少个 的排列 满足如下性质:
- 。
- ,存在一个包含 中至少一个元素的置换环经过了 。
考虑容斥去掉第一个限制(枚举前 个点中哪些违反了限制)。此时有两种做法:
- 优秀的组合做法(std)
考虑将元素依次插入置换环中,前 个元素有 种方式。考虑接下来的 个元素,它们必须满足插入到已有的环中,而不能自己新增一个环。因此可以发现此时的第 个元素有 种方案。最后的 个元素没有限制,因此此时的第 个元素有 种方案。最后即可得到答案为 。这可以预处理后 计算,因此原问题可以在 内解决。
- 不优秀的容斥做法
考虑容斥,枚举若干个不合法的环,这些环满足环中存在至少一个 的点,且不存在 的点。枚举 个环的系数为 。
设这些环内有 个 的点和 个 的点。考虑枚举 ,计算一种 的所有情况带来的贡献之和。此时的所有方案为 个点的置换,满足每个环都包含一个 间的点。一个方案的贡献为 ,其中 为环的数量。
考虑再次容斥,枚举一些只包含 中的环,每个环再乘以 的系数,这样就去掉了所有限制。设这部分的点数为 ,则对于一个 ,这些点间一个环的系数为 ,这部分方案数为 。考虑没有容斥的部分,这部分有 个点,每个环系数为 。这相当于对于每个排列求和 ,其中 为这个排列的环数。设 表示 的逆序对数,可以发现如果在排列中交换两个数,则 同时改变奇偶性。因此 ,这里 为总点数,从而乘以 后,相当于对于每个排列求和 。这相当于全 矩阵的行列式,因此这部分贡献非零当且仅当点数不超过 ,即 。
因此可以发现只有 的部分贡献系数可能非零。此时只有一个环,容易发现所有方案的贡献系数和为 。(还有不容斥的情况,即 )
回到第一个容斥,可以发现方案数为:
此时右侧求和项内部只与 有关,可以预处理前缀和得到 的做法。
但也可以继续推式子:
因此得到相同结果。复杂度仍然是 。
Code
#include<cstdio>
using namespace std;
#define N 10005000
#define mod 1000000007
int n,k,fr[N],ifr[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d%d",&n,&k);
fr[0]=1;for(int i=1;i<=n;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[n]=pw(fr[n],mod-2);for(int i=n;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
for(int i=1;i<=k;i++)
{
int rs=k-i-(i<k);
for(int j=0;j<i;j++)as=(as+1ll*(j&1?mod-1:1)*fr[i]%mod*ifr[j]%mod*ifr[i-j-1]%mod*fr[i-j+rs+n-k]%mod*ifr[i-j+n-k]%mod*fr[i-j+n-k-1])%mod;
}
printf("%d\n",as);
}
AGC045E Fragile Balls
Problem
有 个盒子和 个球。初始时第 个球在第 个盒子中。你可以进行如下操作:
选择一个包含至少两个球的盒子,将这个盒子中的任意一个球拿出来,移动到任意一个盒子中。
每个球有一个移动次数限制:第 个球最多被移动 次,这里 。
给定一个目标状态,在目标状态中第 个球在第 个盒子中,保证此时每个盒子中至少有一个球。
你需要通过操作达到目标状态且不超过每个球的移动次数限制。求最少的操作步数或输出无解。
且数据满足上述限制
Sol
首先考虑没有移动次数限制的情况。显然答案的下界是满足 的球个数,考虑什么时候能达到这一下界。
如果要达到这一下界,则每次移动必须直接让球达到终点。因此考虑将盒子看作点,对于每个球连边 (不连自环)。设当前点 上的球数量为 ,则一次这样的移动可以看成,选择一条边 ,满足 ,然后将 减一, 加一并删去这条边。能达到下界当且仅当能删去所有边。
此时可以选出一条 到 不经过重复边的路径,满足 ,然后依次操作路径上的边,这样可以将 减一, 加一然后删去整条路径。
从路径角度考虑,可以发现如果能将图分成若干条路径,使得对于每个点 ,从 出发的路径条数不超过 ,则一定能达到下界,只需要依次操作每条路径即可(事实上通过一些调整可以证明这也是必要条件)。
考虑图的一个连通块(将边看成无向边后的连通块),可以发现如下结论:
能达到下界当且仅当连通块中有一个点满足 。
证明:设第 个点入度为 ,出度为 。如果有一些点满足 ,则考虑对于每一个 的点连出 条路径,这些路径以 的点结尾。可以发现直接从每条路径起点开始 dfs
即可得到满足条件的路径(否则与入度出度矛盾),此时剩下的边使得每个点入度等于出度,因此剩下的边组成的每个连通块间存在有向欧拉回路。因为原图连通,因此当前的每个回路必定与某条之前取出的路径相邻,那么可以把所有回路拼接到路径上,这样就构造出了路径。
再考虑合法性的问题,初始时在 上的球数量至少有 个,因此如果 ,则这样显然合法。否则,不存在一个球满足 。但因为题目保证终止状态中每个盒子内至少有一个球,因此此时存在一个球满足 ,因此仍然合法。这也说明在这种情况中一定存在 的点。
接下来考虑每个点都满足 的情况,此时存在有向欧拉回路。如果有一个点满足 ,则从这个点开始走欧拉回路即可达到目标。
否则,如果所有点都满足 ,则每个点只连出一条边,因此这种情况构成了一个环 ,对于每个 有一个球初始在 ,它要去往 。但这个环内无法进行操作,只能有一个其它球放过来将这个环处理掉。因此这种情况不能达到下界。
从而每个不是单个环且全部 的连通块都可以在只将所有 的球移动一次的情况下解决。
考虑剩余的环,这个环必须依靠其它球才能解决。考虑将一个球移动到这个环上,然后就可以依次消掉环上的边,最后再将这个球移出去即可。
因此考虑找到上一部分中的一次移动 ,设环上点为 ,则可以改为 ,且在经过环时处理环上的情况。对于多个环的情况,可以改为 ,因此让上一部分一个移动过的球多移动一次即可处理一个环。可以发现每个环都需要这样的一次处理。
但有可能不存在移动过的边(例如全部是自环),此时考虑选择一个所在点满足 的球,让它从 开始经过所有需要的点再回到 。可以发现选择它需要额外的一步。
这样就解决了 的情况(最后有一些细节被省略了),接下来考虑原问题。
因为 ,可以发现上面过程中每个不是环的连通块的移动方式仍然合法且最优,因此这部分可以使用这种方式移动。
因此此时仍然需要对于每个环,让一个球多移动一步来处理这个环。可以发现不同的球有以下几种情况:
- 这个球是一个能直接处理的连通块中的一条边,且 。此时这个球能进行 次处理环的操作。
- 这个球满足 ,但它在一个能直接处理的连通块中。由上一部分的讨论可以发现这个球可以进行 次处理环的操作,但如果使用这个球则需要 的额外步数。
- 一些球对应的边组成一个长度大于等于 的环,这个环需要一次操作来处理(需要 的步数)。可以发现处理之后环上的球都变成了第一种情况,因此处理后这个环会带来 次可用的处理环操作。
- 一个球满足 但它不在一个能直接处理的连通块中,可以发现这种情况是上两种情况的混合。此时可以不考虑这个球,但也可以向上一种情况那样用一次操作处理这个球,然后像第二种情况那样使用这个球,此时需要一次操作和 的额外步数,可以带来 次处理环的操作。(如果对这个球使用了操作但不使用这个球,则一定不优)
则一种合法操作相当于处理完第一部分后,经过一些上述操作将每个第三种情况的环都操作一次,且每一次操作时都还有操作次数。此时还有一些顺序问题,例如第二种情况中的一个球可能需要在当前连通块操作完之前使用,但如果需要使用这个球,则可以在接下来的操作中先使用这个球再使用连通块中的其它操作。可以发现这里一定可以通过一些调整忽略掉顺序问题,只需要考虑上面给出的操作次数的问题。
此时第二类球相当于增加额外代价(步数)来增加操作次数,因此一定可以在处理了第一类情况后先使用一些这类球,再做剩下两类操作。
而做第三类情况时每个环需要一次操作,因此任意时刻剩余操作数都不是 。而第四类球相当于先使用一次操作再加上一些操作次数,因此一定先用第四类最后考虑第三类。
考虑最后做第三类的情况,显然这一类当中按照每个环的 从大到小做最优(事实上先做大于 的就行)。因此这一类可以排序后扫一遍,得到至少需要多少步剩余操作进入这部分才能做完这部分。
然后考虑之前的两部分。此时问题相当于需要将剩余步数增加到至少等于某个值,两种操作分别会导致一些额外步数,求最少需要多少额外步数。
此时一个问题是第四类的球需要一步操作才能使用,但注意到只要有第三类环,则至少需要有一步操作,且这里让剩余操作步数变小一定不优,因此如果处理完第一类情况后有可用步数,则这部分可以不考虑需要一步操作的限制,只需要将得到的步数减一,而如果处理完第一类后没有剩余步数,则一定需要选一个第二类的球,此时可以先选择第二类中给的步数最多的球,变为之前的情况(不存在则无解)。(这里还有不存在第三类环的情况,但此时显然不需要之后的所有操作,可以直接特殊处理)。
经过上述处理,可以发现后半部分问题变为如下形式:
你当前剩余操作次数为 ,你需要将剩余操作次数增加到 。有两种方式:
- 选择一个第二类的球,增加 的额外步数,然后操作次数增加 。
- 选择一个第四类的球,增加 的额外步数,然后操作次数增加 。
最小化增加的总额外步数。
可以发现这是一个体积为 的背包问题,可以分别对两类排序后扫一遍解决。
这里也可以进行贪心,考虑将物品按照价值除以重量排序,显然每一类中选择最优的前若干个。考虑重量为 的物品,如果选了一个重量为 的物品但有两个重量为 且更优的物品没有选,则一定不优。同理如果有一个重量为 的物品没有选但选择了两个重量为 且更不优的物品则也不优。
因此考虑按照价值除以重量从大到小选物品,由上述讨论可以得到最优解一定由这个过程中某个状态或者这个状态多选或少选一个重量为 的物品得到。因此扫一遍即可得到答案。
复杂度
Code
感谢数据没有卡 sum c_i 爆int的情况
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define N 100400
int n,m,s[N][3],fa[N],is[N],is2[N],di[N],si[N],c1,rs,li,as,ls=1e8;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
vector<int> v0,v1;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&s[i][0],&s[i][1],&s[i][2]);
fa[finds(s[i][0])]=finds(s[i][1]);di[s[i][0]]++;
}
for(int i=1;i<=n;i++)if(di[i]>1)is[finds(i)]=1;
for(int i=1;i<=m;i++)if(s[i][0]==s[i][1])
if(is[finds(s[i][0])])v0.push_back(s[i][2]-1);
else v1.push_back(s[i][2]-2);
else if(is[finds(s[i][0])])rs+=s[i][2]-1,as++,rs=rs>1e8?1e8:rs;
else si[finds(s[i][0])]+=s[i][2]-1,as++,is2[finds(s[i][0])]=1,si[finds(s[i][0])]=si[finds(s[i][0])]>1e8?1e8:si[finds(s[i][0])];
for(int i=1;i<=n;i++)if(is2[i])si[++c1]=si[i];
v0.push_back(-1e8);v1.push_back(-1e8);
sort(v0.begin(),v0.end());
sort(v1.begin(),v1.end());
sort(si+1,si+c1+1);
if(!c1){printf("%d\n",as);return 0;}
if(!rs)
{
rs+=v0.back();v0.pop_back();as++;
if(rs<=0){printf("-1\n");return 0;}
}
for(int i=c1;i>=1;i--)
{
if(rs)rs--;else li++;
rs+=si[i],as++;
}
while(li>0&&li<1e7)
{
if(li<=v0.back()){as++,li=0;break;}
if(v0.back()*2>=v1.back())as++,li-=v0.back(),ls=v0.back(),v0.pop_back();
else as+=2,li-=v1.back(),v1.pop_back();
if(li+ls<=0){as--;break;}
}
printf("%d\n",li>0?-1:as);
}
AGC045F Division into Multiples
Problem
有 个 和 个 ,你需要将这些数分成若干组,使得每个数正好在一个组中。
称一个组是好的当且仅当它里面所有数的和是 的倍数且它非空。求最多能得到的好的组的个数。
多组数据,
Sol
问题可以看成,用这些数组合出尽量多的好的组,求最多能组出多少个好的组,
考虑先对问题做一些处理。如果 ,则显然可以将 同时除以 。
否则,如果 而 ,设 ,则一个好的组中若干个 相加是 的倍数,和也是 的倍数,因此这个组中所有 的和必须是 的倍数。因为 ,因此一个好的组中 的数量必须是 的倍数。从而可以将 除以 并将 除以 下取整,由上述分析这样答案不变。
同样地可以对 进行考虑,这之后可以得到 的问题。注意这里可能的细节,例如处理后得到 。
考虑此时一个好的组,设这个组由 个 和 个 组成。这里使用 表示这一个组。显然,如果 ,则分出一个 一定更优。同理如果 则分出一个 一定更优。因此只需要考虑 以及 的所有 。
此时因为 且 分别与 互质,可以发现对于一个 ,方程 有唯一解 。因此可以找到一个数 ,使得这部分只需要考虑 种组:对于每个 ,有 。
考虑这些组中哪些是有用的。注意到如果存在两个好的组 满足 ,则 一定是无用的(换成 更优)。考虑这些 ,考虑一个序列 ,则一个 是有用的当且仅当 是序列 的前缀最小值。
考虑求前缀最小值,一种直接的方式是从第一个位置开始,每次找到接下来第一个 小于它的位置。设当前位置为 ,则相当于找到最小的正整数 使得 ,即 。可以发现这个限制相当于 ,跳到这个位置后, 会增加 ,而对应的 会减小 。
可以发现随着 的减小,方程 的限制会变严,因此解出的最小 一定单调不降,可以发现 单调不增(否则沿用上一组解合法且更优)。
在二维平面上考虑这个问题,相当于从 开始,每次找到一个 ,向右下走 。这样构成了一条折线,需要的点为上面的所有整点。同时因为 单调不降, 单调不增,可以发现这个折线构成了一个下凸壳。
设这上面的整点为 ,可以发现这些点间满足 的差分递增, 的差分(是负数)也递增。考虑将 也加入序列,经过一些讨论可以发现加入后也满足这个性质,这样就考虑了所有的组。从而如果在折线上选出了两组 且 ,则考虑换成 ,由 的下凸性可以发现这样不会变差。因此存在一组最优解,使得最优解只使用了 中相邻的两组。
考虑一条从原点到 的射线。如果选择的两种组对应的点都在射线一侧,如果都在折线上方,可以发现使用它们进行组合时只需要考虑 的限制,因此这种情况下选择凸壳在折线上方的部分中 最小的点最优,可以发现这个点即为射线与凸壳的交点向左上遇到的第一个整点。同理如果都在折线下发,则只会使用交点向右下遇到的第一个整点。
因此可以发现,找到射线与凸壳的交点,取交点在凸壳上向两个方向遇到的第一个整点,此时一定存在一组最优解只使用了这两个点对应的组。
考虑如何求出凸壳的信息以及这两个点。由于每次选择的 一定递增,因此在求出了一个 后,可以一直用 直到这个 不满足条件,即再减 会让它变为负数。可以发现这样会让 对当前求出的 取模。可以发现只会取模 次,因此整个过程中只会有 次改变 ,从而下凸壳可以分为 条线段,所有需要的组即为这些线段上的所有整点。
考虑如何求出 ,即求出 的最小正整数解。可以发现这类 形式的方程可以使用类欧几里得算法在 的复杂度内得到解,下面简要描述这种方式:
显然可以在问题中将 对 取模,因此不妨设 。问题相当于需要找到一组非负整数 使得 ,且最小化 。由 ,每个 最多对应一组 ,且最小的合法 一定对应最小的合法 。
可以发现限制能重写为 。考虑向两侧不断加上 。如果存在 的合法解,则可以直接求出最小 。否则,可以加到右侧区间两个边界大于等于 。此时限制相当于 。因为不存在 的合法解,此时一定有 。此时问题变为一个类似形式,但 进行了交换的问题。而处理这个问题时可以将交换后的 对 取模,因此这是一个相互取模的过程,由欧几里得算法这样只会进行 步,因此复杂度为 。
使用上述算法,可以 求出这条折线。然后考虑找到交点,可以先找到凸壳与射线相交的线段,然后在线段上对整点二分找到交点两侧的组。最后一步也可以通过一些计算 直接得到。
那么剩下最后一个问题:只有两种可以用的组 ,求最优解。
问题相当于找到 ,满足 ,最大化 。
在二维平面上考虑这两个限制。找到两条分界线的交点 ,则交点向左上有一条斜率小于等于 的射线,向右下有一条斜率大于等于 的射线(由 的单调性),两条射线左下的半平面(加上 的限制)即为 可行的区域。
设交点坐标为 。由于左侧部分上限斜率小于等于 ,因此如果一组 合法且满足 ,则 一定合法。同理,如果它满足 ,则 一定合法。因此在最大化 时,只需要考虑 两种情况。从而这部分可以 解决。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 41
#define ll long long
int T,a,b,x,y,c;
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int exgcd(int a,int b,int &x,int &y)
{
if(!b){x=1;y=0;return 1;}
int g=exgcd(b,a%b,y,x);y-=a/b*x;
return g;
}
int getinv(int a,int p)
{
int x,y;
exgcd(a,p,x,y);
return (x%p+p)%p;
}
int solve(int a,int b,int l,int r)
{
if((l-1)/a<r/a)return (l-1)/a+1;
int as=solve(b%a,a,a-r%a,a-l%a);
return (1ll*b*as+l-1)/a+1;
}
int st[N],ra[N],ct,vl;
void solve()
{
scanf("%d%d%d%d%d",&a,&x,&b,&y,&c);
int g=gcd(gcd(a,b),c);
a/=g;b/=g;c/=g;
g=gcd(a,c);a/=g;c/=g;y/=g;
g=gcd(b,c);b/=g;c/=g;x/=g;
a%=c;b%=c;
if(!a&&!b){printf("%d\n",x+y);return;}
if(!a){printf("%d\n",x+y/c);return;}
if(!b){printf("%d\n",y+x/c);return;}
vl=1ll*a*getinv(b,c)%c;
ra[0]=1;st[ct=1]=(c-1)/vl;
while(1)
{
int ls=1ll*st[ct]*vl%c;
if(ls==c-1)break;
ra[ct]=solve(vl,c,c-vl,c-vl+(c-ls))+1;
int v1=1ll*ra[ct]*vl%c;
st[ct+1]=st[ct]+(c-ls-1)/v1*ra[ct];
ct++;
}
ra[ct]=c-st[ct];st[++ct]=c;
vl=c-vl;
int nw=0;
while(nw+1<ct&&1ll*st[nw+1]*y<=(1ll*st[nw+1]*vl%c)*x)nw++;
int lb=0,rb=(st[nw+1]-st[nw])/ra[nw]-1,as=0;
while(lb<=rb)
{
int mid=(lb+rb)>>1;
int si=st[nw]+ra[nw]*mid;
if(1ll*si*y<=(1ll*si*vl%c)*x)as=mid,lb=mid+1;
else rb=mid-1;
}
int s1=st[nw]+ra[nw]*as,s2=s1+ra[nw];
int t1=1ll*s1*vl%c,t2=1ll*s2*vl%c;
if(!t1)t1=c;
//s1*a+s2*b<=x t1*a+t2*b<=y s1<s2 t1>t2
int va=(1ll*y*s2-1ll*x*t2)/(1ll*t1*s2-1ll*t2*s1),vb=(1ll*x*t1-1ll*y*s1)/(1ll*t1*s2-1ll*t2*s1);
int as1=va+(x-s1*va)/s2,as2=vb+(y-t2*vb)/t1;
if(as1<as2)as1=as2;
printf("%d\n",as1);
}
int main()
{
scanf("%d",&T);
while(T--)solve();
}
AGC044C Strange Dance
Problem
有 个人,编号为 。初始时第 个人在位置 。
接下来有 次操作,每次操作为以下两种之一:
- 考虑每个人,如果这个人当前在位置 ,则将其移动到位置 。
- 考虑每个人,设这个人当前在位置 ,考虑 的三进制表示,将三进制表示下的 变为 , 变为 得到 ,则这个人会移动到位置 。
在 次操作后,求出所有人的位置。
Sol
考虑维护所有位置的三进制表示。第二种操作对三进制表示的变换是显然的。考虑第一种操作对三进制表示的改变。
可以发现加一相当于将后缀为 的变为 (以下记为 ),类似地还有如下变换:。这样的变换有 组,每一组都形如:将后缀为 的所有表示的这段后缀改为 。
因此考虑快速找到后缀为某个字符串的所有表示,从而可以使用翻转 Trie,即将所有三进制表示翻转后插入 Trie。这样一个整体加一操作只需要在 Trie 上不断走 儿子,然后对到达的每个点的儿子进行一些交换即可。
此时操作 可以看成在 Trie 上整体做标记(标记每个点的儿子需要做一些交换),且这个标记可以下传,这样就可以同时进行 操作。在每个叶子上维护它对应的编号即可得到答案。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1080050
int n,vl[N],lz[N],ch[N][3],as[N];
char s[N];
void build(int x,int d)
{
if(d==n)
{
int v1=x,tp=0;
while(v1>1)
tp=tp*3+v1%3,v1/=3;
vl[x]=tp;return;
}
for(int i=0;i<3;i++)ch[x][i]=x*3+i,build(x*3+i,d+1);
}
void modify(int x)
{
if(!ch[x][0])return;
if(lz[x])
{
lz[x]=0;
swap(ch[x][1],ch[x][2]);
for(int i=0;i<3;i++)lz[ch[x][i]]^=1;
}
swap(ch[x][1],ch[x][2]);swap(ch[x][1],ch[x][0]);
modify(ch[x][0]);
}
void dfs(int x,int d)
{
if(!ch[x][0])
{
int v1=d,tp=0;
while(v1>1)
tp=tp*3+v1%3,v1/=3;
as[vl[x]]=tp;return;
}
if(lz[x])
{
lz[x]=0;
swap(ch[x][1],ch[x][2]);
for(int i=0;i<3;i++)lz[ch[x][i]]^=1;
}
for(int i=0;i<3;i++)dfs(ch[x][i],d*3+i);
}
int main()
{
scanf("%d",&n);
build(1,0);
scanf("%s",s+1);
for(int i=1;s[i];i++)
if(s[i]=='S')lz[1]^=1;
else modify(1);
dfs(1,1);
int m=1;for(int i=1;i<=n;i++)m*=3;
for(int i=0;i<m;i++)printf("%d ",as[i]);
}
AGC044D Guess the Password
Problem
交互题。
你需要猜一个长度不超过 的密码(保证非空),密码可能包含大小写字符和数字,即 。
你可以进行不超过 次询问,每次询问你可以给出一个长度不超过 的字符串 ,交互库会返回 与密码间的编辑距离。这里编辑距离定义为只使用插入,删除和修改字符,至少需要多少步使得两个串相等。
Sol
编辑距离非常复杂,考虑从编辑距离中找到合适的信息。
可以发现如果知道了密码长度 ,则对于一个字符串 ,你可以通过判断答案是否是 来得到 是否是密码的子序列。
首先考虑求出 ,考虑将每种字符问一遍,则如果这种字符出现,结果显然为 ,否则结果为 。因为至少有一种字符出现,取所有询问的最小值加一即可得到长度。
然后考虑对于一种字符 ,询问两个 ,三个 ,以此类推直到当前结果不是密码的子序列,这样就得到了每种字符出现的次数。(注意 个相同字符的情况,这时直接问会询问 个字符,但这样不合法需要特判)
考虑向一个子序列中插入一个字符。一种方式是枚举序列中每个空位,尝试插入并询问插入后是否是子序列,直到找到一个插入位置使得接下来还是子序列。但直接插入所有字符询问次数无法接受。
考虑归并两种字符 ,当前所有出现过的 构成了一个子序列,考虑将 依次插入,找到第一个位置满足将 插入到这个位置时得到的序列是密码的子序列。则因为所有的 都在这个子序列中,因此这个位置必须插入一个 。然后将这个 插入,从这个位置向后找第一个能插入下一个 的位置。可以发现这样只需要 次操作就能归并两种字符,得到密码中只保留这两种字符后得到的序列。
可以发现上述方法也适用于归并两种不交的字符集。即对于 ,如果找到了密码中只保留 中字符得到的字符串和只保留 中字符得到的字符串,考虑上述归并,仍然找到第一个可以插入的位置,因为所有 中字符都在当前串种,因此这个位置必须插入,然后向后尝试插入下一个字符并继续即可。
因此可以对所有字符分治后归并,可以发现这个过程类似于归并排序,不难发现操作次数是 的。注意一些细节(比如跳过归并最后一个字符的过程)后可以发现这正好不会超过 步。
Code
#include<cstdio>
#include<vector>
using namespace std;
int n,l1,le,is[67];
char s[67]="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
int query(vector<int> si)
{
if(si.size()>128)return 0;
printf("? ");
for(int i=0;i<si.size();i++)printf("%c",s[si[i]]);
printf("\n");fflush(stdout);
int as=0;scanf("%d",&as);return as==n-si.size();
}
vector<int> merge(vector<int> a,vector<int> b)
{
vector<int> as;
int sa=a.size(),sb=b.size();
int la=0;
for(int i=0;i<sb;i++)
{
vector<int> as2=as;
as2.push_back(b[i]);
for(int j=la;j<sa;j++)as2.push_back(a[j]);
if(la==sa||query(as2))as.push_back(b[i]);
else as.push_back(a[la]),la++,i--;
}
for(int i=la;i<sa;i++)as.push_back(a[i]);
return as;
}
vector<int> sr[66];
int main()
{
int mi=1e9;
for(int i=0;i<=61;i++)
{
printf("? %c\n",s[i]);fflush(stdout);
scanf("%d",&is[i]);mi=mi>is[i]?is[i]:mi;
}
for(int i=0;i<=61;i++)is[i]=is[i]==mi;
n=mi+1;
for(int i=0;i<=61;i++)if(is[i])
{
vector<int> as;
while(1)
{
as.push_back(i);
if(as.size()>1&&!query(as))break;
}
as.pop_back();
sr[i]=as;
}
while(1)
{
int f=-1,g=-1;
for(int i=0;i<=61;i++)if(sr[i].size()&&(f==-1||sr[i].size()<sr[f].size()))f=i;
for(int i=0;i<=61;i++)if(sr[i].size()&&i!=f&&(g==-1||sr[i].size()<sr[g].size()))g=i;
if(g==-1)break;
if(f>g)f^=g^=f^=g;
sr[f]=merge(sr[f],sr[g]);sr[g]={};
}
vector<int> as;
for(int i=0;i<=61;i++)if(sr[i].size())as=sr[i];
printf("! ");
for(int i=0;i<as.size();i++)printf("%c",s[as[i]]);
printf("\n");fflush(stdout);
}
AGC044E Random Pawn
Problem
有一个 个点的环,每个点有权值 。你需要进行如下游戏:
有一个棋子随机放在环上一个点处,你知道棋子的位置。设棋子当前位置在 ,你可以选择如下操作:
- 花费 的代价让游戏继续,随后棋子随机移动到当前位置两侧位置中的一个。
- 选择结束游戏。你的分数为 减去之前花费的总代价。
求最优策略下你的最大期望收益。
Sol
显然每个位置上会选择继续或停止中的一种,因此策略可以看成一个 ,使得如果棋子到了 中位置则选择停止,否则选择继续。
考虑 最大的位置,显然到这个位置会选择停止,因此可以从这个位置分开将环变为链,从而变成一个链上的问题,此时移动到链端点则必须停止,即必须选择两端。
首先考虑 的链上问题。考虑选择停止的相邻两个位置 。则从 中某个位置出发只会停留在 中的一个位置。设 表示从 出发停留在 的概率,则有 。
这个方程一定有唯一解,可以发现 是一个解,因此它是方程的唯一解。
考虑解的几何意义,可以发现这相当于从 到 的一条折线。因此问题可以看成,有一列点 ,你需要选择一条向右的折线,这条折线的权值定义为折线在 的高度之和,求最大权值。
可以发现取上凸壳一定最优,因此这种情况下求出上凸壳即可得到答案。
回到 的链上问题。考虑将问题形式变为上一种情况。从 的角度考虑,此时的问题相当于 ,而之前的问题没有这个 。
注意到 是线性的,考虑设一个偏移量,即令 ,则方程可以看成 。
则只需要取一组 使得 ,,然后对 求凸包即可。显然扫一遍即可求出 ,然后由凸包可以求出 之和,最后即可得到答案。复杂度 。
另外一种(乱搞)方式是猜想它仍然是某种形式的凸壳,又注意到求凸壳的方式可以看成如果 则 不在凸壳上可以被删去,这里 是 这条线段对答案的代价。因此考虑同样用栈维护当前选择的点,加入一个点时,先认为选择了这个点,此时如果删去栈顶使答案更优(即 )则删去栈顶,重复这个过程。
考虑计算包含 情况的一个 ,此时需要计算从中间随机一个位置出发开始随机游走,到边界停止,经过中间每个位置的期望次数。考虑将从中间每个位置开始的期望计算一次。设 表示这个期望,则有方程 。可以发现一组解为 ,因此预处理 即可得到 。上一种方式证明了这确实是一个凸包,因此这种做法也能保证正确性且复杂度相同。
Code
第一种代码🕊了。
#include<cstdio>
using namespace std;
#define N 200500
#define ll long long
int n,st[N],ct,fr;
ll v1[N],v2[N];
ll v[N],s0[N],s1[N],s2[N],as;
ll calc(int l,int r)
{
ll as=(r-l)*(v[l]+v[r]);
as+=(s0[r]-s0[l])*l*r*2;
as-=(s1[r]-s1[l])*(l+r)*2;
as+=(s2[r]-s2[l])*2;
return as;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%lld",&v1[i]),fr=v1[fr]<v1[i]?i:fr;
for(int i=1;i<=n;i++)scanf("%lld",&v2[i]);
for(int i=1;i<=n+1;i++)
{
v[i]=v1[(i+fr-2)%n+1];
s0[i]=v2[(i+fr-2)%n+1];
s1[i]=s1[i-1]+s0[i]*i;s2[i]=s2[i-1]+s0[i]*i*i;
s0[i]+=s0[i-1];
}
for(int i=1;i<=n+1;i++)
{
while(ct>1&&calc(st[ct],i)+calc(st[ct-1],st[ct])<=calc(st[ct-1],i))ct--;
st[++ct]=i;
}
for(int i=1;i<ct;i++)as+=calc(st[i],st[i+1]);
printf("%.14lf\n",0.5*as/n);
}
AGC044F Name-Preserving Clubs
Problem
给定 ,称若干个 的子集(可以重复)是好的,当且仅当在子集无序的情况下,可以通过这些子集将 两两区分(重排人后仍然可以区分)。即可重集 是好的当且仅当 ,其中一个子集的 定义为对每个子集中的元素作用 后得到的集合。
求有多少种本质不同的 满足条件且在满足条件的基础上使用集合的数量 最少。两个方案被认为是相同的当且仅当重排人后可以让它们相同,即 。但如果答案大于 ,只需要输出 。
Sol
首先考虑 大概是多少。即使集合可区分, 个集合也只能区分 个数,那么 最多能解决 的问题。事实上 时 一定不行:如果凑到 ,所有数在集合中的出现情况一定用到了 的全部子集(如果两个数所出现在的集合完全一样,它们显然不能被区分),但这样所有集合间都不能区分:情况相当于所有 位二进制数都出现了,显然交换两位不改变任何情况。
再考虑 至少能解决多少的问题,手玩一下 (最多能做到 )或者其它情况,可以发现 一定可以:考虑构造大小从 到 的集合,这样集合大小自动区分了所有集合,然后按顺序划分,每一步将之前还没区分开的每组数分到两个大小不超过 ( 为剩余集合数量) 的就行。事实上上界比这个更优,但第一个情况出现在 ,而这是不可能手玩的。
那么可以发现 还大概是 的,只是在接近 时可能需要多 。
回到原问题,样例给了一个恶心的情况:,有一组解存在两个相同的集合,这实在过于奇怪。但根据之前的分析可以发现,如果有两个相同集合,那么它的区分能力不超过 个可区分的集合,即 ,但这时的 和几乎只有本来上界的一半,因此可以猜想此时答案会很大(感受一下越远离上界限制应该更松)。那么可以先不考虑这种情况,先做限制 中不存在重复的情况。
经过一些手玩可能可以得到通向下面限制的思路,例如考虑 接近 的情况,假装所有集合可以区分,每加入一个集合可以看成对所有当前还不能分开的数的集合做一次划分。划分后的集合大小显然不能超过 ,那么除了 个空位,剩余的划分都几乎是满的,因此可以考虑通过空位在划分中移动的情况描述解。问题是这样不能直接对集合重排去重,但这可以启发从空位角度分析。然后可以打表发现 ,或者直接看出补集。
另一种思路是,把之前的分析过程写出来,事实上写出前几段大概也能发现一些事情。
设 表示限制 中不存在重复,固定 后一个 的答案。首先考虑如何描述一种 的方案,所有子集可以看成若干个 的二元组,每一个表示第 个集合包含 。那么集合不同的限制相当于重排所有二元组的 后不会使得二元组集合相同。原限制为重排 后集合的可重集不同,这里因为集合不相同,那么两个集合相同当且仅当可以通过一种集合重排从一个到另一个。从而可以发现,合法当且仅当对于任意一种重排 同时重排 的方案,它得到的结果都和初始状态不同(除非两个重排都选择平凡排列)
将形式写出来之后,可以直接发现如下结论:
证明:根据合法条件的定义,显然 两侧是完全对称的。
证明:对于一个 ,反转所有子集是否出现,即可得到 的一组方案,且这显然是双射。
这样可以化简一些情况,但最后仍然需要算一个 ,但通过化简可以认为 。
猜想答案很快就超过 了。事实上暴力可以发现 ,那么可以更相信这一点。\sout{然后信仰就行了}。通过一些高明的构造可以发现如下结论:
如果 ,那么
根本想不到的构造:反过来考虑 ,考虑这样的 个集合:,其中最后一个表示 的补集。那么可以通过大小唯一确定最后一个集合,然后可以确定 ,接下来通过依次考虑与当前集合的交即可确定 (这些二元组构成一条链),这样重排就唯一了。
然后考虑向里面任意加大小不是 的子集,这样之前的判定仍然有效,那么它是合法的。且因为元素唯一确定,任意两个这样的方案显然不重复。那么这就有
种方案。同时将所有集合取补集可以发现还有对称且不同的另一类。(这里都用到了 ,因为 会有问题)
考虑这个组合数的最小值,显然在 的最值处取到。 最大时下标为 ,反过来相当于 。但 时容易发现 ,那么 时下标是 一定更小。从而最小值在 取到,答案是 。
然后考虑暴力算剩下的 。这里暴力的效率甚至不需要考虑时限,因为可以打表。
直接的想法是枚举 二元组出现了哪些。那么暴力是 ,考虑判定合法,如果直接枚举 的重排则复杂度难以接受,考虑回到原题面,只重排一侧,然后考虑所有集合的可重集是否相同,这可以看出二进制数后用桶解决。那么判定的复杂度为 。这样在 的情况下应该可以在若干秒内解决。注意到这里根本没考虑去重,因此答案需要除以 ,那么也可以剪枝:方案数大于 就退出。
也可以进行一些去重上的优化:例如考虑依次枚举 个 的子集,然后要求选择的 个子集满足某种顺序(例如二进制表示递增),这样就去掉了 的复杂度。
还可以继续尝试处理 :一种状态可以看成一个 位的 串,依次表示每个子集是否出现。之前的合法判定相当于每次重排 位后不能回到原状态。但可以发现一些简单的性质:两个串重排 位后可能得到的 串集合如果相交则相等。(事实上这个结论等价于群作用中轨道的结论)简单的证明是,如果两个重排都能到同一个串,那么这个串反向做一种重排即可得到一个串可以重排到另一个串。然后复合一下即可发现一个串能重排到的另一个串也可以。那么考虑统计那些满足条件,且重排到的 个串中自身是字典序最小的。那么每个这样的串代表一个轨道(可以得到的串的集合)。因此可以考虑使用之前的方式搜,但如果重排后字典序更小则删去这种情况。这样也不用除 了, 可以跑进 ,可以发现 到 都远大于
事实上暴力跑出来 ,最后两组分别大概用了 ( 太大了跑不出来)。那么这就解决了 中两两不同的问题。
最后考虑存在相同元素。根据之前的分析,即使只有一对相同,它能解决的问题也不超过 ,那么此时对应的情况 ,其中 。可以发现 时这个情况一定不能化简,那么 时一定是 , 时需要算 ,但这远大于了 。那么只需要考虑 ,可以发现情况只剩 ( 时上界分别是 )。
再写一个暴力,可以发现分别有 种本质不同的方案。注意这里的实现与之前略有不同。加上去就结束了。
复杂度
Code
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
#define N 753
long long n,m;
int is[N],p[N],s2[N],as;
bool chk(int n)
{
for(int i=1;i<=n;i++)p[i]=i;
while(next_permutation(p+1,p+n+1))
{
int fg=1;
for(int j=1;j<1<<n;j++)s2[j]=0;
for(int j=1;j<1<<n;j++)if(is[j])
{
int su=0;
for(int i=1;i<=n;i++)if((j>>i-1)&1)su|=1<<p[i]-1;
s2[su]=1;
if(is[su]!=is[j])fg=0;
}
for(int j=1;j<1<<n;j++)if(is[j]<s2[j])break;else if(s2[j]<is[j])return 0;
if(fg)return 0;
}
return 1;
}
void dfs(int u,int r, int n)
{
if(as>1000)return;
if(u+1<r||r<0)return;
if(u==-1){as+=chk(n);return;}
is[u]=0;dfs(u-1,r,n);
is[u]=1;dfs(u-1,r-1,n);
}
int solver(ll n,ll m)
{
as=0;
if(n>5)return -1;//(5,5) -> 976
dfs((1<<n)-1,m,n);
return as>1000?-1:as;
}
int solve(ll n,ll m)
{
if(m<0)return 0;
if(n>m)return solve(m,n);
if(n&&m>(1ll<<n-1))return solve(n,(1ll<<n)-m);
return solver(n,m);
}
int main()
{
scanf("%lld",&n);
int as=0;
while(!as)as=solve(n,++m);
/*
for duplicate sets: x<=2^k but answer is k+1
answer for 14 => (14,5) => > 10^6
(8,4) impossible
(4,3) => {1,2}*2 {1,3}
(7,4) => {1,2,3}*2 {1,4,5} {1,2,4,7} or {1,2,3,4}*2 {1,2,5,6} {2,3,5}
*/
if(n==4)as++;if(n==7)as+=2;
printf("%d\n",as);
}
AGC043C Giant Graph
Problem
给三个点数为 ,边数为 的无向图。现在考虑一个 的图。图中点编号为 ,其中 。这个图中有如下三种边:
- 对于第一个图中的一条边 和任意 , 间有一条边。
- 对于第二个图中的一条边 和任意 , 间有一条边。
- 对于第三个图中的一条边 和任意 , 间有一条边。
点 的权值为 。你需要选出若干点,使得这些点在图上构成一个独立集且点权和最大。输出最大点权和模 的结果。
,保证图是简单图。
Sol
关于我做这题的时候手推了一遍贪心过程中的 f(a,b)=mex{f(i,b)(i<a),f(a,i)(i<b)},然后发现结果是异或并且这就是SG相加这事
可以发现图有如下两条性质:
- 对于每个 ,点权为 的点数小于 。
- 不存在一条边的两端点权值相同。
由第一条,一定是先最大化选择 的点数,然后最大化 的点数,以此类推。再由第二条,只需要按照点权从大到小考虑所有点(点权相同时任意排列)并尝试加入独立集即可得到最优解。
可以发现此时的贪心为如下形式:
按照 从大到小考虑所有点,对于一个点 ,如果有一个点与它相邻且点权大于它且它被选了,则这个点不能选,否则会选择这个点。
如果将点权大于它且相邻的点看作它的后继,则这个形式可以看成,如果一个点的后继都没有被选则可以选择这个点,否则不能选择这个点。
可以发现这等价于一个博弈的胜负判定过程(\sout{如何想到可以参考第一句}),因此问题可以变成如下博弈问题:
从当前点出发,可以选择走到一个相邻且点权大于它的点。双方轮流操作,不能操作者输。一个点能被选入独立集当且仅当这个点满足先手必败。
再考虑这个图的形式,可以发现在大的图上移动一步相当于在三个图中选择一个,在这个图上将对应点向后移动。因此问题变为如下形式:
有三张图,每张图上有一个棋子,每次操作的人可以选择一个图上的棋子,将其移动到图上与之相邻且编号更大的点,不能操作者输,一个 能被选入独立集当且仅当初始棋子在 时先手必败。
由 SG 函数的性质,可以对于每张图分别计算 SG,最后的 SG 为三个图上棋子 SG 的异或,先手必败当且仅当异或为 。则答案为 ,其中 表示一个图上一个点的 SG 值。
图上计算 SG 的方式为后继取 mex,由此可以归纳得到如果图中有一个 SG 值为 的节点,则至少需要 条边。因此三个图中的 SG 都是 级别的,可以对应每种 SG 统计这种 SG 的点权和,然后暴力计算即可。
复杂度
Code
#include<cstdio>
#include<vector>
using namespace std;
#define N 100500
#define M 531
#define mod 998244353
int n,m,a,b,vl[N],dp[N],vis[N],as;
int f[M],g[M],h[M];
vector<int> rs[N];
void calc(int *f)
{
for(int i=1;i<=n;i++)rs[i].clear(),vis[i]=0;
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
if(a>b)a^=b^=a^=b;
rs[a].push_back(b);
}
for(int i=n;i>=1;i--)
{
for(int j=0;j<rs[i].size();j++)vis[dp[rs[i][j]]]=1;
for(int j=1;j<=n;j++)if(!vis[j]){dp[i]=j;break;}
for(int j=0;j<rs[i].size();j++)vis[dp[rs[i][j]]]=0;
}
for(int i=1,vl=1;i<=n;i++)vl=1000000000000000000ll%mod*vl%mod,f[dp[i]]=(f[dp[i]]+vl)%mod;
}
int main()
{
scanf("%d",&n);
calc(f);calc(g);calc(h);
for(int i=1;i<=512;i++)for(int j=1;j<=512;j++)
as=(as+1ll*f[i]*g[j]%mod*h[((i-1)^(j-1))+1])%mod;
printf("%d\n",as);
}
AGC043D Merge Triplets
Problem
给定 ,求有多少个 的排列 可以被如下方式构造出来:
构造 个长度为 的序列 ,使得 中每个元素正好在这些序列中出现一次。
接下来进行 次操作,每次选择当前所有序列开头中最小的一个,将这个元素删除并放到 结尾,这样即可得到一个 阶排列 。
答案对给定质数 取模。
Sol
考虑由一组 得到 的过程。可以发现在一个序列 中,如果前一个元素大于后一个元素,则显然选择前一个元素后会立刻选择后一个元素。更进一步可以发现,对于每个序列,考虑在序列的所有前缀最大值之前分段,这样每个序列被分成了若干段,可以发现每一段内部一定在最后的 中连续。
然后考虑不同段的情况,可以发现此时一个序列中的不同段满足段的开头元素递增。因此可以发现将当前的所有点按照开头元素从小到大排序并拼接,得到的排列一定满足条件且最优。
考虑在得到的排列 上做上述分段,可以发现这样分成来的段正好是之前得到的所有段(因为之前将所有段按照开始位置递增排序)。因此可以发现如下结论:
对于一个排列 ,它能被表示当且仅当它进行上述分段后,分出来的段可以拼接成 个长度为 的序列。
考虑如何满足这个条件,显然 分出的每一段长度不能超过 ,同时可以发现每一个长度为 的序列中最多有一个长度大于等于 的段,因此可以得到一个限制为长度大于等于 的段数量小于等于 。
另一方面,如果满足这个条件,则考虑先将 每个单独放入一个序列,剩余的全部由长度 填补,显然可以拼出 个长度为 的序列,因此这是必要条件。
因此考虑设 表示当前前面的若干段总长为 ,且有 个长度大于等于 的段的方案数,转移有三种:
- 加入一个长度为 的段,方案数为 (它是前缀最大值)
- 加入一个长度为 的段,方案数为 。
- 加入一个长度为 的段,方案数为 。
直接转移即可,复杂度
Code
#include<cstdio>
using namespace std;
#define N 6050
int n,p,dp[N][N],as;
int main()
{
scanf("%d%d",&n,&p);n*=3;
dp[0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;j<=n/3;j++)if(dp[i][j])
dp[i+1][j]=(dp[i+1][j]+dp[i][j])%p,
dp[i+2][j+1]=(dp[i+2][j+1]+1ll*dp[i][j]*(i+1))%p,
dp[i+3][j+1]=(dp[i+3][j+1]+1ll*dp[i][j]*(i+1)*(i+2))%p;
for(int i=0;i<=n/3;i++)as=(as+dp[n][i])%p;
printf("%d\n",as);
}
AGC043E Topology
Problem
给定 ,考虑如下问题:
在一个二维平面上有 个点,点 在 。
考虑一条封闭且不经过给定点的曲线 ,定义 为 能否在不经过给定点的情况下,连续形变到一个点。(另一种理解方式:将点看成垂直于平面的柱子,求能否将曲线取下)
对于 的一个子集 ,定义 表示只保留 中点时的 。
给定长度为 的序列 ,构造一条曲线 ,满足 。可以证明 等价于只经过 且每一步在相邻两点间移动的曲线,输出曲线。
Sol
考虑如何简单地描述曲线 。
考虑以 的部分为基准,曲线可以由若干步如下段拼接成的环给出:
- 顺时针方向绕过 ,记为 。
- 逆时针方向绕过 ,记为 。
然后可以发现,保留 中点相当于保留 中 对应的 ,剩余部分形成的环。
再考虑曲线可以连续形变到一个点,即可以将环删空的条件。可以发现如下结论:
可以做到当且仅当可以通过若干步如下操作将环删空:
选取一对相邻的 或 ,删去这一对。
证明:首先可以发现环上一段的连续形变相当于加入或删除一对 。然后可以发现加入操作是无用的:考虑合法方案的最后一次加入操作,考虑之后的删除中删去这两个加入元素的操作,一定可以删去这次加入,换为这两次操作删去的另外两个元素配对删去。
此时可以发现如下性质:
如果保留 可以做到,则保留 的一个子集 一定可以做到。
证明:考虑用操作 的过程操作 ,删除在 中不存在的对时直接跳过,否则直接做。
那么可以发现,如果 ,则一定无解。
考虑一个简化的情况: 不能做到,但它的任意真子集都能做到。
可以发现对应不存在上一种无解情况的 ,一定可以找到若干集合 ,使得 当且仅当 不是任意一个 的子集。因此如果能对每个 构造出来,它们的并很可能可以满足条件。
考虑一个的构造,即使用 连接成一个环,满足去掉任意一种 对应的 后,环可以通过之前的操作删空,但初始状态不能被删空。
手玩可以发现, 一个解为 , 一个简单的解为 。
对于 的情况。考虑 的构造形式可以发现如下构造:
记 的答案为 ,记 表示翻转字符串,同时将 互换得到的字符串。那么有 。
可以发现 中不存在相邻的一对能删除的字符,因此初始状态一定不能删空。而如果去掉一种字符,去掉前 种会导致 删空,留下 也被删空,而去掉最后一种会导致串变为 ,那么从中间开始可以删完。
这样得到了合法构造,长度为 。
考虑多个集合的情况。如果直接拼接,可能导致两个不同集合的段互相相消。这里给出一种解决方式(实际上可能随便写一个都是对的)
考虑在构造时按照编号从小到大加入,这样一个串中开头元素的编号最小,结尾元素的编号最大,然后进行拼接。合法的 显然可以消掉,对于不合法的 ,操作顺序不影响删的结果,先删掉能被完全删掉的段,剩余段内部不能做任何操作,如果两段的开头结尾互相消掉了,则因为开头最小,结尾最大,这两段不能再做任何操作。如果不考虑长度为 的段,剩余段长度至少是 ,因此所有段之间不能再做操作。如果有一个长度为 的段不能消掉,那么由奇偶性就不能删空。
因此将每一段的构造拼接起来即可。
元素个数不超过 ,连接两个元素需要 段,因此复杂度为
Code
//The Journey Never Ends.
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define N 259
int n,ty[N],nw;
char s[N];
vector<int> si;
vector<pair<int,int> > as;
int main()
{
scanf("%d%s",&n,s);
for(int i=1;i<1<<n;i++)if(s[i]=='0')
{
int fg=0;
for(int j=(i-1)&i;j;j=(j-1)&i)if(s[j]=='0')fg=1;
if(fg)continue;
int tp=(1<<n)-1-i;
for(int j=tp;j;j=(j-1)&tp)if(s[i+j]=='1'){printf("Impossible\n");return 0;}
vector<int> ti;
for(int j=1;j<=n;j++)if(i&(1<<j-1))
if(!ti.size())ti.push_back(j);
else
{
int fi=ti.size();
ti.push_back(j);
for(int k=0;k<fi;k++)ti.push_back(ti[fi-k-1]);
ti.push_back(j);
}
for(int i=0;i<ti.size();i++)si.push_back(ti[i]);
}
as.push_back(make_pair(0,0));
for(int i=0;i<si.size();i++)
{
int u=si[i];
int f=u-1+ty[u],g=u-1+(!ty[u]);
while(nw!=f)nw+=nw<f?1:-1,as.push_back(make_pair(nw,0));
as.push_back(make_pair(f,1));as.push_back(make_pair(g,1));as.push_back(make_pair(g,0));
nw=g;ty[u]^=1;
}
while(nw!=0)nw+=nw<0?1:-1,as.push_back(make_pair(nw,0));
printf("Possible\n%d\n",as.size()-1);
for(int i=0;i<as.size();i++)printf("%d %d\n",as[i].first,as[i].second);
}
AGC043F Jewelry Box
Problem
有 个商店,第 个卖 种珠宝,每种珠宝有它的大小 ,价格 以及可用的数量 。
如果 个珠宝满足如下条件,则它们可以构成一个好的组合:
- 第 个珠宝来自商店 。
- 有 个给定的限制,每个限制形如 ,表示第 个珠宝的大小必须小于等于第 个珠宝的大小加上 的结果。
现在有 个询问,每次询问给定一个 ,求购买一些珠宝组成 个好的组合时需要花费的最小总价,或输出无解。
Sol
可以发现一个大的困难在于困难需要组出 组,因此甚至直接描述方案中每一组珠宝的选择都是不行的。考虑有没有更好的描述方式。注意到每组的限制是相同的类似偏序的结构,容易发现如下结论:
记第 组中的第 个珠宝大小为 ,则对于任意一组解,重排珠宝使得所有 单调不降后其仍然是一组解。
证明:考虑每一个 的限制,只考虑这两个的大小可以看成有两个序列 ,其对应位置满足 ,这类似于需要对位小于,那么显然应当排序做。从而排序对于每个限制都不会变差。
因此只需要考虑 递增的方案。那么此时 只有 种取值且递增,因此考虑转而记录分界点:将每个商店的珠宝按照大小排序,记 表示第 个商店的前 种珠宝选了多少个。从 的角度考虑,可以发现限制相当于如下不等式:
- 每一个商店内的限制:
- 给定限制:对于一个 ,从分界点考虑,对于 商店的每一个 ,它相当于限制 的长度为 的前缀的值不超过 ,这相当于找到 中大小最大但不超过 的珠宝 ,然后限制相当于 ,即小于等于 的至少要选这么多个。
- 与询问相关的限制:
那么限制都是一些形如 的形式,其中 表示某两个 。再考虑代价,显然是
这是一大堆与 相关的形式,可以由此想到一个东西:
考虑如下最大费用循环流的问题:每条边有起点终点 。流量上限 ,费用 。问题为找到给每条边一个流量,使得每个点流量平衡,且最大化总费用。
容易将其写成线性规划的形式:记点 的出边集合为 ,入边集合为 ,则有:
\max. &\sum_j x_jc_j\\ s. t. &\sum_{j\in I_i}x_j-\sum_{j\in O_i}x_j=0, &\forall i\\ &x_j\leq f_j, &\forall j
(这里线性规划默认要求 )
那么考虑线性规划的对偶。这里点和边分别有一组限制,记点限制对偶后的变量为 ,边限制对偶后的变量为 ,那么对偶后可以发现问题变为:
\min. &\sum_j f_jq_j\\ s. t. &p_{t_j}-p_{f_j}+q_j\geq c_j&\forall j\\
由于原来点的限制是等于,因此对偶后 的符号不限,但 。
那么此时 的取值一定尽量小,即 。从而问题变为,找到一组 ,最小化 。这被称为最大费用循环流的对偶形式。
可以发现对偶后的问题和当前题目导出的限制相似。首先 的限制可以写成最小化 ,然后代价在有前面的大小限制的情况下可以写成 ,从而该问题可以被表示为最大费用循环流的对偶形式。
那么考虑对偶回去。因此把每个 变为一个点,为了简便将所有 合并为一个点 (因为它们必须相等),将所有 合并为另一个点 。则所有限制重写为:
- 等价于 ,因此连边 ,流量 ,费用 。
- 类似地等价于 ,因此连边 ,流量 ,费用
- 对于给定限制的部分,每个限制形如 ,那么连边 ,流量 ,费用 。
- 对于代价,可以看成 ,那么连边 ,流量 ,费用
- 对于两侧的权值限制,可以发现整体平移不影响问题,因此只需要看成 ,那么就是 ,即连边 ,流量 ,费用 。
可以发现除去最后一条边外,剩余边边权都非正。因此循环流中不存在不经过 边的环。那么可以发现,考虑删去 的边,记 表示从 到 流量为 的最大费用。则答案为 。根据对偶的结论,如果答案是 则表示对偶前无解。
费用流模型是凸的,因此很好解决。考虑直接费用流,对于每个 记录加入第 个流量后最大费用改变的值为 ,这在正常费用流的过程中容易求出,且是一个单调不增的序列。那么求一个 的答案只需要找到最后一个 的数,然后对前面的数求和 即可。
但一个小问题是这里有流量为 的边,因此某个距离上可能出现 的流量。可以发现如果费用流里面某一步出现了 ,那么相当于有 个 等于当前距离,那么大于 的询问都无解,小于等于的询问和这 个流量无关,那么只需要在第一次遇到 时停止即可。
考虑这样最多会做多少步。如果有一个流量的路径上所有边流量都是 ,那么它就能做到 的流量。因此在停止前,每个流量必须经过一条流量不是 的边。但可以发现这样的边只有代价的一种,且流量为 ,因此停止前的总流量不超过 。可以发现题目保证了 很小。
那么最后只需要把费用流做出来,且需要做的流量不大。为了简便,可以将所有边权取反(事实上也可以反向来建图),变为最小费用但所有边权都是正数。那么有一个经典的消负环+dijkstra做法:
假设初始图没有负边权,如果有,则先做一遍任意能做负权边的最短路。
记当前从源点到点 的最短路为 ,考虑改变所有边的边权:对于 的边,其边权加上 。那么经过该调整后,由最短路的性质所有边权变为非负。且这样的调整不改变最短路的性质:这样调整后每条从源点到 的路径总边权都正好减少了 (相邻项相消),那么源点到任意一点的最短路等于调整后的最短路加上这次调整的 。
可以发现调整后源点到每个点的最短路都是 ,那么此时求这个费用的流量只需要在边权为 的边上进行(因为剩余边边权都是正的),此时问题变为一个网络流,可以任意用一种算法解决。
考虑处理了这个费用的流量后怎么开始处理下一个费用。在之前的残量网络上重新跑最短路,此时只有边权为 的边可能被翻转,因此边权还是非负,那么可以 dijkstra 求出新的最短路。然后考虑在边权上按照新的最短路再做一次调整(也可以看成把两次的 加起来),这样总距离增加了,再调整后又满足边权非负且到汇点的最短路是 ,那么重复这两步,直到停止。
最坏复杂度为 ,因为可能每个流量费用都不同,但大多数情况下上界还是优于直接用负权最短路的做法。
在本题使用该做法,则流的复杂度为 。询问可以通过处理 的前缀和再二分解决。因此复杂度为 ,可以通过。
Code
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
#define N 34
#define M 925
#define S 59180
#define ll long long
int n,m,q,ci[N],sz[N][N],wi[N][N],lb[N],a,b,c;
ll vi[N][N],ct,as[S],si[S],k;
int su,head[M],cur[M],cnt=1,dep[M],vis[M];
ll dis[M];
struct edge{int t,next,v;ll c;}ed[S];
void adde(int f,int t,int v,ll c)
{
ed[++cnt]=(edge){t,head[f],v,c};head[f]=cnt;
ed[++cnt]=(edge){f,head[t],0,-c};head[t]=cnt;
}
bool dij(int s,int t)
{
for(int i=1;i<=su;i++)dis[i]=2e18,vis[i]=0;
priority_queue<pair<ll,int> > qu;
dis[s]=0;qu.push(make_pair(0,s));
while(!qu.empty())
{
int u=qu.top().second;qu.pop();
if(vis[u])continue;vis[u]=1;
for(int i=head[u];i;i=ed[i].next)if(ed[i].v&&dis[ed[i].t]>dis[u]+ed[i].c)
dis[ed[i].t]=dis[u]+ed[i].c,qu.push(make_pair(-dis[ed[i].t],ed[i].t));
}
return vis[t];
}
bool bfs(int s,int t)
{
for(int i=1;i<=su;i++)dep[i]=-1,cur[i]=head[i];
queue<int> qu;
dep[s]=0;qu.push(s);
while(!qu.empty())
{
int u=qu.front();qu.pop();
if(u==t)return 1;
for(int i=head[u];i;i=ed[i].next)if(ed[i].v&&!ed[i].c&&dep[ed[i].t]==-1)
dep[ed[i].t]=dep[u]+1,qu.push(ed[i].t);
}
return 0;
}
int dfs(int u,int t,int f)
{
if(u==t||!f)return f;
int as=0,tp;
for(int &i=cur[u];i;i=ed[i].next)
if(!ed[i].c&&dep[ed[i].t]==dep[u]+1&&(tp=dfs(ed[i].t,t,min(f,ed[i].v))))
{
ed[i].v-=tp;ed[i^1].v+=tp;
as+=tp;f-=tp;
if(!f)return as;
}
return as;
}
void dij_mcmf(int s,int t)
{
ll sd=0;
while(dij(s,t))
{
sd+=dis[t];
for(int u=1;u<=su;u++)
for(int i=head[u];i;i=ed[i].next)ed[i].c+=dis[u]-dis[ed[i].t];
int si=0;
while(bfs(s,t)&&si<S)si+=dfs(s,t,S);
if(si>=S){as[++ct]=sd;break;}
for(int i=1;i<=si;i++)as[++ct]=sd;
}
for(int i=1;i<=ct;i++)si[i]=si[i-1]+as[i];
}
int main()
{
scanf("%d",&n);su=2;
for(int i=1;i<=n;i++)
{
scanf("%d",&ci[i]);lb[i]=su;su+=ci[i]-1;
for(int j=1;j<=ci[i];j++)scanf("%d%d%lld",&sz[i][j],&wi[i][j],&vi[i][j]);
for(int p=1;p<=ci[i];p++)for(int q=ci[i];q>p;q--)if(sz[i][q]<sz[i][q-1])
swap(sz[i][q],sz[i][q-1]),swap(wi[i][q],wi[i][q-1]),swap(vi[i][q],vi[i][q-1]);
for(int j=1;j<=ci[i];j++)
{
int li=j==1?1:lb[i]+j-1,ri=j==ci[i]?2:lb[i]+j;
adde(li,ri,1e9,vi[i][j]);adde(li,ri,wi[i][j],0);
adde(ri,li,1e9,0);
}
}
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
int nw=0;
for(int j=1;j<=ci[a];j++)
{
while(sz[a][j]+c>=sz[b][nw+1]&&nw<ci[b])nw++;
int li=j==ci[a]?2:lb[a]+j,ri=nw==0?1:nw==ci[b]?2:lb[b]+nw;
adde(ri,li,1e9,0);
}
}
dij_mcmf(1,2);
scanf("%d",&q);
while(q--)
{
scanf("%lld",&k);
if(k>as[ct]){printf("-1\n");continue;}
int li=lower_bound(as+1,as+ct+1,k)-as-1;
printf("%lld\n",li*k-si[li]);
}
}
AGC041D Problem Scores
Problem
给定 。现在有 道题目,你需要给每道题目分配一个正整数分数 ,满足如下两条限制:
- 对于任意一个 ,任意 道题目的分数和严格大于任意 道题目的分数和。
求方案数,对给定质数 取模。
Sol
在 单调不降的限制下,第二个限制可以看成对于任意 都有 。
可以发现在 时,不等式两侧会有一些部分抵消掉,而抵消之后就变成了 的的限制。因此只需要考虑前一半限制。
又因为 不降,可以发现 时,如果 的限制被满足,则 的限制一定被满足。因此只需要考虑一个限制,即 时的限制。
此时可以发现限制相当于有一个序列 , 中前 项为 ,后 项为 ,然后要求 。为了简便,考虑加入一个元素 ,并令 ,这样不改变结果。
因为 单调不降,考虑 的差分,记 ,则有:
记 ,可以发现 ,其中 为奇数时 有一个,偶数时 有两个。此时问题变为统计有多少组 满足如下限制:
如果给 全部减一并取相反数,则最后一个限制变为 。可以发现此时的 排序后为一个 以及 个正数,第 个正数为 。而此时 ,因此可以将 减一,变为求满足 的非负 组数。
此时 ,可以发现确定了 后即可唯一确定 ,且 一定小于等于 ,因此一定存在一个合法的 。
从而问题变为,统计有多少种非负整数 满足 。做一个完全背包即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5050
int n,p,dp[N];
int main()
{
scanf("%d%d",&n,&p);
dp[0]=1;
for(int i=0;i<=n;i++)
{
int tp=(i+1+(!i))/2;
for(int j=tp;j<=n;j++)dp[j]=(dp[j]+dp[j-tp])%p;
}
printf("%d\n",dp[n-1]);
}
AGC041E Balancing Network
Problem
有 条平行直线,在这些直线间从左向右依次有 个连接器。第 个连接器连接第 和 条直线。
你需要给每个连接器指定一个方向。在指定方向后,考虑如下形式的运动:
一枚棋子从某条直线的左侧(在所有连接器之前)开始向右运动,如果它经过了一个连接器且这个连接器是从这条直线指向另外一条直线,则棋子会切换到连接器连向的那条直线,否则棋子不会改变所在直线。
称一种定向方式是好的,当且仅当存在一条直线满足无论棋子从哪条直线开始运动,经过所有连接器后棋子一定在这条直线上。
完成以下两个任务:
- 找到一个好的定向方式或者说明不存在。
- 找到一个不好的定向方式或者说明不存在。
Sol
首先考虑好的定向方式,即从所有起始点出发可以到达同一个终点。
考虑判定一个终点的情况。从后往前考虑每个连接器,对于每条直线维护从当前位置向右出发能否到达终点。初始只有终点所在的直线满足这一要求,合法当且仅当能通过定向使得每条直线都满足要求。
考虑加入一个连接器的情况,如果连接器两侧直线状态相同,可以发现这个连接器任意定向不影响这些状态。否则,两侧有一条直线当前向后可以到达终点,另外一条直线不行。可以发现此时连接器从不能到达的连向能到达的,就可以让两条直线都变为能到达的,这样一定最优。因此可以这样 的贪心判断一个终点是否合法。
可以发现这个判定条件等价于不考虑连接器定向,从一个终点出发向左走,可以经过连接器,这样能到达所有终点。因此问题变为判断是否有一个终点出发向左走能到达所有起点。
从左向右考虑,设 表示当前第 条直线向左能到达哪些起点。则加入一个连接器 时,只需要将 同时变为 。最后一个终点合法当且仅当 是全集。使用 bitset 维护即可。最后还原方案可以使用上述讨论。(这里也可以继续倒着做,并且两种方式等价)
时间复杂度 ,空间复杂度
然后考虑不好的定向方式,即存在一对起始点,使得它们到达的终点不同。
考虑什么情况下能满足条件。显然从一对起始点 出发,它们遇到的第一个连接器不能都是 ,这样一定无解。
另一方面,可以发现只要满足这个条件,这对起始点一定可以到达不同的终点。
证明:考虑对连接器数量 归纳。考虑找到 向后遇到的第一个连接器,不妨设这个连接器为 。考虑 向后遇到的第一个连接器,如果这个连接器同时连接 ,则在 处让 指向 ,这样之后的路径起点变为 而 遇到的下一个连接器为 ,满足条件。否则在这里让 指向 ,接下来起点仍然是 而 遇到的下一个连接器为 ,因此也满足条件。因此可以归纳得到一定能满足条件。
那么只需要找到这样的一对 ,再用证明中的方式构造解即可。可以发现 时一定无解,否则可以取 , 为 中 第一个遇到的连接器指向的点之外的点,这样一定满足条件。
这里需要在不断向右的过程中找某条直线从当前位置向右的第一个连接器,而这容易做到均摊线性。
复杂度
Code
#include<cstdio>
#include<bitset>
#include<vector>
#include<algorithm>
using namespace std;
#define N 50050
int n,m,t,s[N*2][2],vis[N];
vector<int> si[N];
bitset<N> dp[N];
char as[N*2];
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=m;i++)scanf("%d%d",&s[i][0],&s[i][1]);
if(t==1)
{
for(int i=1;i<=n;i++)dp[i].set(i,1);
for(int i=1;i<=m;i++)dp[s[i][0]]=dp[s[i][1]]=dp[s[i][0]]|dp[s[i][1]];
for(int i=1;i<=n;i++)if(dp[i].count()==n)
{
vis[i]=1;
for(int j=m;j>=1;j--)
{
int f=s[j][0],t=s[j][1];
if(vis[f])as[j]='^',vis[t]=1;
else as[j]='v',vis[f]|=vis[t];
}
printf("%s\n",as+1);return 0;
}
printf("-1\n");return 0;
}
for(int i=m;i>=1;i--)for(int t=0;t<2;t++)si[s[i][t]].push_back(s[i][!t]);
if(n==2){printf("-1\n");return 0;}
int a=1,b=(si[1].size()&&si[1].back()==2)?3:2;
for(int i=1;i<=m;i++)
{
int f=s[i][0],t=s[i][1];
si[f].pop_back();si[t].pop_back();
if(!(a==f||a==t))swap(a,b);
if(!(a==f||a==t)){as[i]='^';continue;}
if(si[b].size()&&si[b].back()==a)as[i]=a==f?'v':'^',a=f^t^a;
else as[i]=a==f?'^':'v';
}
printf("%s\n",as+1);
}
AGC041F Histogram Rooks
Problem
有一个 行 列的网格,给定 ,现在对于每一列只保留这一列最下方的若干个格子,对于第 列只保留最低的 个格子,这样得到一个部分网格,
你需要在剩余的网格上放若干个车。定义一个车能攻击到的位置为与它在同行或同列,且与它的线段间没有被删去的格子的所有格子。
你需要通过放置这些车,使得每个格子上最多放一个车,且每个剩余格子都至少被一个车攻击到。求放置的方案数,答案模 。
Sol
可以发现从上往下做没有好的性质,考虑从下往上处理。
考虑最下面的一行,如果这一行放了至少一个车,则这一行的所有格子都能被攻击到,只需要判断之后的行是否合法(删去这行后可能网格变成多个连通部分,对每个部分显然可以分开处理)。此时下面对上面判断合法有用的信息只有哪些列上放了车,但在下面的所有行上这些位置等价,因此可以只记录有多少列上有车。
而如果这一行没有放车,则会要求在所有列上都至少需要放一个车。可以发现满足这个要求后上面的每个格子显然都能被攻击到,因此接下来只需要考虑这一限制,可以发现这个限制也只需要考虑下面部分有多少列放了车。
因此考虑设 表示只考虑 之间的列,只考虑 的行,这些列中有 个列在下面部分放了车且下面部分都合法时,对于所有 种选择放车的列的方式求和上面部分合法的方案数。设 表示与之前相同的条件,但变为要求最后每一列上都放了车的方案数。
考虑转移,与之前相同地,考虑删去最后一行,那么有以下几种情况:
- 之间的转移,注意到 再向上只考虑哪些列上有车,因此考虑枚举这一列之后有车的列数 ,则显然 ,考虑如何从 转移到 。注意到 统计了每一种选择 行在下面放车的方案数,而每一个这样的状态可以由 个之前下面有 行放车的状态到达,且每种状态到达的方案数为 (之前的位置可以任意放)。因此转移过来的系数为 。
- 之间的转移,可以发现转移与 几乎相同,除去限制不能全部不填。可以发现全部不填的情况只在 时出现,因此此时需要将转移系数减一。
- 从下一行的 转移到这一行的 ,这种情况即为在 的状态下这一行全部不填,因此为 到 ,系数为 。
然后考虑删去一行后,网格分成若干部分的情况。对 分别讨论:
考虑转移 ,相当于需要在这些列中选择 列为下面放了车的列。考虑分出的一部分 ,则如果在这一部分里面选择了 列,则方案数为 。再考虑变成空位的那些列,每一列可以选或者不选。将它们合并起来的过程可以看成做背包或者看成 OGF 的乘法,例如将分出的每一部分看成一个 ,每一个空行看成 ,相乘即可得到 。
然后考虑 ,可以发现情况和上一种类似,只是转移到的变为 ,同时每个空行必须被下面选(否则不满足要求)。
这样就完成了整个过程。注意到一个高度上只会做若干个两两不交的区间,每个区间内的第一类转移复杂度是长度的平方,因此总和是每层 的,而第二类转移的复杂度和在笛卡尔树上做背包合并一样是总共 的,因此总复杂度为
Code
#include<cstdio>
#include<vector>
using namespace std;
#define N 405
#define mod 998244353
int n,v[N],c[N][N],p2[N];
vector<int> mul(vector<int> a,vector<int> b)
{
int sa=a.size(),sb=b.size();
vector<int> as;as.resize(sa+sb-1);
for(int i=0;i<sa;i++)for(int j=0;j<sb;j++)as[i+j]=(as[i+j]+1ll*a[i]*b[j])%mod;
return as;
}
struct sth{vector<int> s0,s1;};
sth solve(int l,int r,int h)
{
int ct=0;
for(int i=l;i<=r;i++)if(v[i]==h-1)ct++;
if(!ct)
{
sth si=solve(l,r,h+1),sr;
sr.s0.resize(r-l+2);sr.s1.resize(r-l+2);
for(int i=0;i<=r-l+1;i++)sr.s1[i]=si.s0[i];
for(int i=0;i<=r-l+1;i++)for(int j=i;j<=r-l+1;j++)
sr.s0[i]=(sr.s0[i]+1ll*si.s0[j]*c[j][j-i]%mod*p2[i])%mod,
sr.s1[i]=(sr.s1[i]+1ll*si.s1[j]*c[j][j-i]%mod*(p2[i]-(i==j)))%mod;
return sr;
}
sth sr,s2;s2.s0.push_back(1);s2.s1.push_back(1);
int ls=l-1;
for(int i=l;i<=r+1;i++)if(v[i]==h-1||i==r+1)
{
if(ls+1<i)
{
sth si=solve(ls+1,i-1,h);
s2.s0=mul(s2.s0,si.s0);
s2.s1=mul(s2.s1,si.s1);
}
ls=i;
}
sr.s0.resize(r-l+2);sr.s1.resize(r-l+2);
for(int i=0;i<=r-l+1;i++)for(int j=0;j<=i&&j<s2.s1.size();j++)if(i-j<=ct)
sr.s1[i]=(sr.s1[i]+1ll*s2.s1[j]*c[ct][i-j])%mod;
for(int i=0;i<=r-l+1;i++)for(int j=0;j<=i&&j<s2.s0.size();j++)if(i-j==ct)
sr.s0[i]=(sr.s0[i]+s2.s0[j])%mod;
return sr;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
for(int i=0;i<=n;i++)c[i][0]=c[i][i]=1;
p2[0]=1;for(int i=1;i<=n;i++)p2[i]=2*p2[i-1]%mod;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
sth sr=solve(1,n,1);
printf("%d\n",sr.s1[0]);
}
AGC040D Balance Beam
Problem
有 条线段,每条线段长度均为 。在第 条线段上,A
移动的速度为 单位长度每秒,B
移动的速度为 单位长度每秒。
A
和 B
进行如下游戏:
首先 A
将 条线段按照任意顺序拼接成一条长度为 的链,随后在链上进行如下游戏:
A
初始位置在链的最左侧,B
的初始位置为链上随机位置(随机是实数随机,即可以随机到某条线段中间的一个位置)。两人同时开始向右移动,如果 A
在 B
到达右端前先追上 B
则 A
获胜,否则 B
获胜。
A
会选择一种使自己获胜概率最大的拼接方式,即求 A
最大的获胜概率。输出答案的既约分数表示。
Sol
考虑如何判断能否追上。对于当前的链上的一个位置 ,记 表示 A
从起点出发到达这个位置的用时, 表示 B
从起点出发到达这个位置的用时。设 B
出发的位置为 ,则 B
实际上到达这个位置的用时为 。A
能追上 B
当且仅当存在一个位置使得他能先到达这个位置,即存在一个 满足 ,即 。
因此考虑找到 最小的位置,这个位置一定是某条线段的结尾,设它是第 条线段的结尾。可以发现 A
能获胜当且仅当 小于等于某个值,因此能让 A
获胜的起始位置为一段前缀。
因此一条确定的链上的问题相当于,找到一个起始位置 使得 A
经过前 条线段的时间小于等于 B
从 开始到第 条线段的时间,最大化 的位置。
容易发现这里可以将对满足 最小的 求改为考虑所有的 ,求最大值。这样之后的问题就和线段间的顺序没有太大关系,因此原问题的答案可以使用如下过程求出:
- 选择若干条线段(作为前 条线段),只考虑这些线段。
- 在选择的线段中,选择一条线段作为起始位置所在的线段,并在上面选择一个起始位置。
- 将剩余线段中的一些放在左侧,剩余放在右侧。满足如下条件:
A
经过所有选择的线段的用时小于等于B
经过右侧所有线段的用时加上在起始线段上向右走到右端点的用时。 - 此时的分数为左侧线段的条数加上起始线段上起始点到左侧的距离(获胜概率为这个分数除以 ),最大分数除以 即为答案。
考虑先选择起始位置所在的线段,再决定剩余每条线段的情况(放在左侧或右侧,或者不放)。记 为 B
的用时减去 A
的用时,则条件为 ,考虑剩余每条线段对答案的影响。对于一条线段 ,它放在左侧会给 贡献 ,放在右侧会贡献 。而对于起始线段,设起点位置在这条线段从左向右的位置 ,则它对 的贡献为 。
可以发现选择了其它线段的状态后,存在合法方案当且仅当当前的 。且因为最后一条线段的贡献在 之间,可以发现最优方式一定是在保证最后能合法的情况下让前面选择在左侧的线段数量尽量多。考虑前面部分,相当于在保证 的情况下最大化选择在左侧的线段数量。从让 尽量大的角度可以发现,最优方式是先让 的选择右侧,剩下的不选,接下来选择一些线段变为左侧。一条 的线段变为左侧会让 减少 ,一条 的线段变为左侧会让 减少 (从右侧变为左侧)。那么先让 的选择右侧后,将除去起始线段外的线段按照 从小到大排序后依次选择,直到再加入会使得 停止,最后再计算这条线段上的 即可。
这样就解决了一个初始线段的问题,考虑原问题,可以先将所有线段按照 排序并预处理前缀和,然后二分选的线段数量,这时需要从剩余线段中删去作为初始线段的问题,但这容易处理。这样就解决了原问题。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 100500
#define ll long long
int n,s[N][2],vl[N],id[N],sl,sa,sb,st[N];
ll si,su[N];
bool cmp(int a,int b){return vl[a]<vl[b];}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&s[i][0],&s[i][1]);
if(s[i][0]>=s[i][1])vl[i]=s[i][0];
else vl[i]=s[i][1],si+=s[i][1]-s[i][0];
st[i]=i;
}
sort(st+1,st+n+1,cmp);
for(int i=1;i<=n;i++)su[i]=su[i-1]+vl[st[i]],id[st[i]]=i;
sb=1;
for(int i=1;i<=n;i++)
{
ll s2=si;if(s[i][0]>=s[i][1])s2-=s[i][0]-s[i][1];
if(s2<0)continue;
int lb=1,rb=n,as=0;
while(lb<=rb)
{
int mid=(lb+rb)>>1;
if(su[mid]-(id[i]<=mid?vl[i]:0)<=s2)as=mid,lb=mid+1;
else rb=mid-1;
}
int v1=as-(id[i]<=as);
s2-=su[as]-(id[i]<=as?vl[i]:0);
int va=s2,vb=s[i][1];
if(va>vb)va=vb;
if(sl<v1||(sl==v1&&(!sa||1ll*sa*vb<1ll*sb*va)))sl=v1,sa=va,sb=vb;
}
ll v1=1ll*sl*sb+sa,v2=1ll*sb*n;
ll g=gcd(v1,v2);
printf("%lld %lld\n",v1/g,v2/g);
}
AGC040E Prefix Suffix Addition
Problem
有一个长度为 的序列 ,初始所有位置都是 ,你可以进行如下两种操作:
- 选择一个整数 ,并选择一个单调不降的整数序列 ,随后对于每一个 ,将 加上 。
- 选择一个整数 ,并选择一个单调不增的整数序列 ,随后对于每一个 ,将 加上 。
给定一个长度为 的正整数序列 ,你需要让 ,求出最少需要的操作次数。
Sol
考虑只有第一种操作的情况。考虑差分,令 ,则第一种操作相当于让 减去 ,然后让 加上一些数,满足加的数的总和为 。
考虑最后的序列的差分,那么每个差分为负数的位置必须至少再某次操作中被选为 ,因此操作数量至少是这种位置的数量。另一方面,考虑对每个差分为负数的位置操作一次,然后加上一些数的部分选择前面一些需要差分为正的位置加到需要的值,如果有多个位置选择尽量靠后的。因为序列非负,因此每一个位置的差分后缀和非正,可以发现这样操作一定可以达到目标,因此这个下界可以被达到。
因此如果只使用第一种操作,答案为 。第二种操作的情况同理。
考虑第一种操作使每个位置增加了多少,可以将原问题变为如下问题:
你需要将每个 分成非负整数 (其中 表示第一种操作增加的值),使得 ,最小化如下代价:
直接的做法是设 表示考虑了前 个位置,且 时前面的最小代价,这样是 的。而这个 有如下性质:
- 对于一个 , 是单调不降的。
证明:考虑前面部分关于 的代价,为 ,可以发现如果前面固定,则随着 增加, 会减小,代价一定不降,因此最后的 不降。
证明:考虑一种 的最优方案,直接改为 ,显然代价最多增加 。
因此 最多有三种取值,且它是不降的,可以直接维护分界点,这样状态数只有 ,转移是分段的,也容易做到 。因此复杂度为 。
具体来说,考虑记录当前最小的 值 ,记录等于 的最大下标 以及等于 的最大下标 。
考虑下一步转移。可以发现转移的代价为 。此时有两种情况:
- 如果 ,则转移分界点为 ,因此转移时一个下标的 不会变大。考虑 的一段向 的一段的转移,可以发现此时只需要令 即可。(但还需要注意 不能超过上界 )
- 如果 ,则分界点为 ,可以看成先向左平移 再做上一步操作。但此时可能第一段全部平移到了 左侧,即 ,此时可以让 加一, 变为之前的 , 变为上界 (由上一种转移的过程可以发现此时 的段一定不会也平移出去)。
这样就完成了整个过程,可以发现实现极其简单。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
int n,ls,a,as,f0,f1;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
if(ls<=a)f1=max(f1,f0+a-ls);
else
{
f1=max(f0,f1+a-ls);f0+=a-ls;
if(f0<0)as++,f0=f1,f1=a;
}
ls=a;
if(f0>a)f0=a;
if(f1>a)f1=a;
}
printf("%d\n",as+(f0<a));
}
这里有不少代码是19年的,所以可能代码看起来差距很大
AGC039D Incenters
Problem
以原点为圆心的单位圆上有 个点,第 个点的极角为 ,即它的坐标是 。
从这些点中选出三个不同的点,求出它们组成的三角形的内心。求这 个内心的坐标平均值,输出实数。
Sol
MO Contest.jpg
做法一:鸡爪定理
根据某个定理,有如下结论:
设三角形为 ,取 的角平分线,设其交三角形的外接圆为 ,则内心 在 上且满足 。
证明:我忘了。
设 按照方位角排序,考虑在上述定理中取 作为角平分线的起点并枚举 ,这样可以得到 ,由上述定理,此时 在以 为圆心半径为 的圆上,且 在 上。此时 长度容易求出,那么如果能快速对于所有 求和 相对于 的方位角的 ,即可得到这种情况的贡献。
设两点 相对原点的方位角为 且 ,则由一些圆上性质不难发现 相对于 的方位角为 。此时 为 的方位角, 为 的方位角,考虑用和角公式拆开 ,那么只需要对于原先的所有点按照方位角排序后求一段的 之和,预处理即可。
复杂度
做法二:一个我不知道名字的定理
根据另外一个定理,有如下结论:
设三角形为 ,取弧 中点 , 中点 , 中点 ,则原内心 为三角形 重心。
证明:我不会。
而重心坐标即为三个顶点坐标平均值,因此可以拆开算贡献。那么枚举两个点组成的弧,计算这个中点对答案的贡献即可。复杂度
Code
#include<cstdio>
#include<cmath>
using namespace std;
#define N 3050
int n,l,v[N];
double pi=acos(-1),su[N][2],sx,sy;
int main()
{
scanf("%d%d",&n,&l);
for(int i=1;i<=n;i++)
{
scanf("%d",&v[i]);
su[i][0]=su[i-1][0]+cos(pi*v[i]/l);
su[i][1]=su[i-1][1]+sin(pi*v[i]/l);
}
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{
double ag=pi*(v[i]+v[j]-l)/l;
sx+=cos(ag)*(j-i-1);sy+=sin(ag)*(j-i-1);
double ax=su[j-1][0]-su[i][0],ay=su[j-1][1]-su[i][1];
ag+=pi;
double ty=ay*cos(ag/2)+ax*sin(ag/2),tx=ax*cos(ag/2)-ay*sin(ag/2);
double ag1=pi*(l-v[j]+v[i])/l,ds=sin(ag1)/sin((pi-ag1)/2);
sx+=tx*ds;sy+=ty*ds;
}
double si=1.0*n*(n-1)*(n-2)/6;
printf("%.14lf %.14lf\n",sx/si,sy/si);
}
AGC039E Pairing Points
Problem
在一个圆上有 个点顺序排列。保证对于任意三对点,如果它们满足没有一个点在其中出现多次,则三对点的连线不会交于一点。
你需要将这些点两两配对并在每一对点间连线,得到 条线段,它们需要满足如下限制:
- 所有线段构成一个连通块(相交表示相连),且所有线段不形成环,即这些线段和它们的交点一起形成一个树的结构。
- 给定一个 的对称 矩阵 ,你能在第 个点间进行配对当且仅当 。
求合法的方案数。
Sol
考虑一组合法方案。考虑方案中的一条线段,一定有一些其它线段和它相交(除非 ),同时删去这条线段后图会变成若干个连通块。可以发现,此时的每个连通块中一定正好包含一条与删去的线段相交的线段(如果不存在则不会连通,如果存在多条则成环)。
考虑与它相交的那些线段,由上一条可以发现这些线段两两不交,此时可以发现它们的端点一定是顺序排列的,即设删去的线段为 到 ,则如果将这些线段按照它们和删去线段的交点排序,则排序后这些线段在 一侧的端点一定是从 沿着这一侧向 移动。
考虑 的一侧,这一侧内每条与 相交的线段都对应了一个这一侧内的连通部分。如果这一侧内存在顺序三个点 使得 属于同一个连通部分,而 不属于这个连通部分,此时如果 与这一侧 之外的某个点连通,则这两个连通部分的边相交,矛盾,否则, 所在的连通部分一定在 内,因此这一部分内部一定有一个点连向 的另外一侧,这样这条边也和 相交。因此一定不会存在这种情况,此时可以发现每个连通部分都是一个区间。
这说明,设与 相交的线段数为 ,则一定可以将一侧 划分为 个区间 ,将另外一侧 划分成 个区间 ,使得分出的第 个连通块包含 中所有点,且正好有一条边从 连向 。
考虑这样的一个连通块,同样考虑删去从 连向 的边,设这条边为 ,则可以发现每一侧的情况与之前相同(对于上侧的情况此时的两侧为 ,另外一侧类似),因此接下来的情况和此时类似。
因此考虑 ,设 表示将 连通,且正好有一条边从 连向 的方案数, 表示将 划分为若干个满足 条件的连通块的方案数。则由上述讨论可以从如下方式由 得到 :
同时答案可以通过枚举一条边由 得到。再考虑转移 ,向两个区间的同侧末端同时加入一个区间作为一个新的连通块对应的点集即可。
复杂度 ,但是常数非常小。注意转移顺序问题。
这里还可以发现只会用到那些满足两个区间在某一侧距离为 的 ,因此可以从某个点两侧开始转移 ,这样 的状态数可以减小为 ,但不会影响复杂度。
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 45
long long dp[N][N][N],f[N][N][N][N],n;
char s[N][N];
long long calc(int a,int b,int c,int d)
{
if(f[a][b][c][d]!=-1)return f[a][b][c][d];
long long as=0;
for(int i=a;i!=(b==n*2?1:b+1);i=i==n*2?1:i+1)
for(int j=c;j!=(d==n*2?1:d+1);j=j==n*2?1:j+1)
as+=dp[a][b][i]*dp[c][d][j]*(s[i][j]=='1');
return f[a][b][c][d]=as;
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n*2;i++)scanf("%s",s[i]+1);
memset(f,-1,sizeof(f));
for(int i=1;i<=n*2;i++)dp[i][i][i]=1;
for(int l=2;l<n*2;l++)
for(int i=1;i<=n*2;i++)
{
int j=(i+l-1)%(2*n)+1;
for(int k=i;k!=(j==n*2?1:j+1);k=k==n*2?1:k+1)
for(int s=i==n*2?1:i+1;s!=(k==n*2?1:k+1);s=s==n*2?1:s+1)
for(int t=j==1?n*2:j-1;t!=(k==1?n*2:k-1);t=t==1?n*2:t-1)
dp[i][j][k]=(dp[i][j][k]+dp[s][t][k]*calc(i,s==1?n*2:s-1,t==n*2?1:t+1,j));
}
long long as=0;
for(int i=2;i<=n*2;i++)as+=dp[i==n*2?1:i+1][i-1][1]*(s[1][i]=='1');
printf("%lld\n",as);
}
AGC039F Min Product Sum
Problem
给定 ,使用如下方式定义一个 的矩阵的权值:
对于每一个位置,考虑它所在行以及它所在的列包含的所有元素的最小值,这 个最小值的乘积即为矩阵的权值。
给定 ,现在你需要给矩阵每个位置填 间的整数。求 种方式中矩阵权值的和,对给定质数 取模。
Sol
可以发现,设第 行的最小值为 ,第 列的最小值为 ,则第 行第 列格子的最小值即为 。
那么一个矩阵的权值即为 。在相乘时 可以被拆成 ,因此贡献可以被拆成如下形式:
记 为满足 的行 个数, 为满足 的行 个数,则答案为 。
因此只需要对于每个 知道有多少行列的最小值大于等于它,即可计算贡献。(这里也可以按照最小值从大到小加入行考虑,但这样会导致最后常数太大)
然后考虑对于一组 ,如何计算有多少个矩阵的行列最小值为 。注意到如果将限制换成每一行的最小值大于等于 ,每一列的最小值大于等于 ,则每个格子的限制独立,可以发现此时的答案为 ,这是容易处理的。但原先的要求是等于,因此考虑容斥,将所有限制看成大于等于的形式,然后钦定若干行列将它们最小值的限制加一,这种方式的系数为 ,其中 为选择的行列数量。
此时每行每列有两种状态,如果它没有被容斥选,则它在权值部分的值为 ,在方案数部分的值也为 。而如果它被选,则它在权值部分的值为 ,在方案数部分的值为 。
因此考虑按权值从小到大(或者从大到小)依次加入所有行列,例如从小到大加入,则考虑对于每个 ,先加入值为 并没有容斥的,这之后当前被加入的即为所有在方案数部分值小于等于 的,因此可以在此计算方案数的一步贡献,然后加入值为 且容斥了的,这之后当前被加入的即为所有在权值部分值小于等于 的,因此在此计算权值的这一步贡献。这样就考虑了所有贡献。
因此设 表示当前加入了 行 列的所有方案的权值和(考虑上面在每一步后加入的贡献),每次考虑枚举加入了多少行多少列。因为行列可以任意排列,因此可以看成一次加入 个行时会乘上 ,列同理,最后乘上 作为排列贡献。此时可以发现如果直接枚举加入的行列数量,则单次复杂度 ,不能接受。考虑分步枚举,先转移加入行的情况,再转移加入列的情况,可以发现这样复杂度即为 ,那么对于上面的每一步都这样做即可。
复杂度 ,注意常数问题(这里如果考虑在加入每一行时算贡献,则常数至少加倍)
Code
#include<cstdio>
using namespace std;
#define N 105
int n,m,k,mod,fr[N],ifr[N],sr[N][N*N],dp[N][N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
void doit()
{
for(int i=0;i<=n;i++)
for(int j=m;j>=0;j--)for(int k=j+1;k<=m;k++)
dp[i][k]=(dp[i][k]+1ll*ifr[k-j]*dp[i][j])%mod;
for(int i=0;i<=m;i++)
for(int j=n;j>=0;j--)for(int k=j+1;k<=n;k++)
dp[k][i]=(dp[k][i]+1ll*ifr[k-j]*dp[j][i])%mod;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&k,&mod);
fr[0]=ifr[0]=1;for(int i=1;i<=100;i++)fr[i]=1ll*fr[i-1]*i%mod,ifr[i]=pw(fr[i],mod-2);
for(int i=1;i<=k;i++)
{
int si=1+pw(i,mod-2);
sr[i][0]=1;for(int j=1;j<=n*m;j++)sr[i][j]=1ll*sr[i][j-1]*si%mod;
}
dp[0][0]=1;
for(int t=1;t<k;t++)
{
doit();
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)dp[i][j]=1ll*dp[i][j]*sr[k-t][i*j]%mod*((i+j)&1?mod-1:1)%mod;
doit();
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)dp[i][j]=1ll*dp[i][j]*sr[t][(n-i)*(m-j)]%mod*((i+j)&1?mod-1:1)%mod;
}
doit();
printf("%d\n",1ll*dp[n][m]*fr[n]%mod*fr[m]%mod);
}
AGC038E Gachapon
Problem
你有一个随机数生成器,它会随机生成 间的整数。有一个正整数序列 ,这个随机数生成器每次生成 的概率为 ,且它不同次随机的结果独立。
考虑不断使用随机数生成器生成整数,直到满足如下条件:
对于每个 , 都至少生成了 次。
求随机次数的期望,答案模 。
Sol
这里有 个限制,第 个限制为 至少生成了 次。题目相当于求这些限制被满足的时间的最大值的期望。
考虑 min-max 容斥,即:
由期望线性性这对期望也成立,因此考虑求一个 的期望,即 中限制有一个限制被满足的用时期望。此时不在 中的元素对限制没有影响。可以发现随机到在 中元素的概率为 ,因此考虑求出只随机生成 中元素(概率与对应 成正比)时的期望时间,乘以 即为原情况下的期望时间。
此时只会生成 中元素,要求存在一个 使得 生成了至少 次的期望时间。可以发现此时操作步数一定有限。那么考虑线性性,对于每个 求出生成了 次后仍然不满足条件的概率,求和即为期望操作次数。
而这也相当于对于每种操作完后仍然不满足条件的操作序列求和这种序列出现的概率,即对于所有满足元素在 中,且每个 出现次数小于 的序列,求和 ,其中 为 出现的次数。
考虑 dp,设 表示考虑了前 种数,序列中前面选择了 个数的所有情况的权值和,如果下一种数加入了 个,则转移系数为 。这样就解决了一个 的问题。转移复杂度与背包合并相同,因此为 。
回到原问题,可以发现这个 dp 只有 与整个 有关,且 dp 外的贡献也只和 以及 的奇偶性有关。因此考虑将 dp 和选择 的过程放在一起 dp,设 表示在前 种数中选择了若干种放入 ,序列中前面选择了 个数,且前面的 时,所有情况的系数和(这里在放入数时不乘 ,而是将这个贡献留到最后计算)。那么转移有两种情况:
- 不向 中选择这种数,则状态不变。
- 选择这种数,则 加上 ,容斥系数乘以 ,然后做上一种 中的转移。
最后由 可以得到 dp 外的贡献(),然后即可得到答案。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 405
#define mod 998244353
int n,a,b,sa,sb,c[N][N],dp[N][N],as;
int main()
{
scanf("%d",&n);
for(int i=0;i<=400;i++)c[i][0]=c[i][i]=1;
for(int i=2;i<=400;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
dp[0][0]=mod-1;
while(n--)
{
scanf("%d%d",&a,&b);
for(int i=sa;i>=0;i--)
for(int k=0,tp=1;k<b;k++,tp=1ll*tp*a%mod)
for(int j=sb;j>=0;j--)
dp[i+a][j+k]=(dp[i+a][j+k]+1ll*dp[i][j]*c[j+k][k]%mod*(mod-tp))%mod;
sa+=a;sb+=b-1;
}
for(int i=1;i<=sa;i++)
{
int ir;for(int j=0;j<i;j++)if((mod%i*j+1)%i==0)ir=(1ll*mod*j+1)/i;
int tp=sa;
for(int j=0;j<=sb;j++)tp=1ll*tp*ir%mod,as=(as+1ll*tp*dp[i][j])%mod;
}
printf("%d\n",as);
}
AGC038F Two Permutations
Problem
给定两个 阶排列 ,你需要构造两个 阶排列 ,满足如下限制:
- , 为 中的一个。
- , 为 中的一个。
在此基础上,你需要最大化满足 的 数量。输出这个最大值。
Sol
考虑排列 中的一个环,可以发现在 中这个环内的元素一定全部取 或者全部取 ,即每个环都有两种选择。
考虑需要最大化的东西,可以看成最小化满足 的 数量。那么考虑每一个位置,可以发现这个位置带来的代价可以看成如果 在 中属于的环和 在 中属于的环的状态满足某些限制,则代价增加 。你需要给每个环确定一个状态(取 或者取对应的环),最小化代价。
但直接这样不能做,因此继续考虑这些限制的性质。可以发现如果 ,即 在 中属于的环长度都大于 ,则这个位置上有代价只有可能是两个环同时取 或者 ,即两个环选择不同时一定没有代价。
而对于 的情况,可以发现 这个环没有选择可以做,因此可以看成一个只对 的限制,即如果 所在的环选择了某些条件,则有一个代价。 的情况同理。
此时的所有代价都只与一个环有关或者在两个属于不同排列的环都选择了同一种状态时会出现,而这可以转化为最小割建图。具体来说,将每个环看作一个点(不考虑单点环),对于 中的一个环,如果割去源点向它的边表示它选择 ,否则它选择 。对于 中的一个环,如果割去源点向它的边表示它选择 ,否则它选择 。这样如果一个限制为如果环 选择了 , 也选择了 ,则有 的代价,这个限制可以看成此时 连向 的一条流量为 的边,表示如果之前的割边使得 选择 , 选择 ,则需要额外的代价割这条边。另外一种情况同理。
最后在得到的图上求出最大流即可。可以发现得到的图的形式等价于二分图匹配,因此使用 dinic 的复杂度为
Code
#include<cstdio>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 200500
int head[N],cnt=1,dis[N],p[N],q[N],ct,id[N],id2[N],as,vl[N],cur[N],vl2[N],n;
struct edge{int t,next,v;}ed[N*5];
void adde(int f,int t,int v){ed[++cnt]=(edge){t,head[f],v};head[f]=cnt;ed[++cnt]=(edge){f,head[t],0};head[t]=cnt;}
bool bfs(int s,int t)
{
queue<int> v;
memset(dis,-1,sizeof(dis));
memcpy(cur,head,sizeof(cur));
v.push(s);dis[s]=1;
while(!v.empty())
{
int r=v.front();v.pop();
for(int i=head[r];i;i=ed[i].next)
if(dis[ed[i].t]==-1&&ed[i].v)
{dis[ed[i].t]=dis[r]+1,v.push(ed[i].t);if(ed[i].t==t)return 1;}
}
return 0;
}
int dfs(int u,int t,int f)
{
if(!f||(u==t))return f;
int as=0,tp;
for(int &i=cur[u];i;i=ed[i].next)
if(ed[i].v&&dis[ed[i].t]==dis[u]+1&&(tp=dfs(ed[i].t,t,min(ed[i].v,f))))
{
ed[i].v-=tp,ed[i^1].v+=tp,as+=tp,f-=tp;
if(!f)return as;
}
return as;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&p[i]),p[i]++;
for(int i=1;i<=n;i++)scanf("%d",&q[i]),q[i]++;
for(int i=1;i<=n;i++)if(!id[i]){id[i]=++ct;for(int j=p[i];j!=i;j=p[j])id[j]=ct;}
for(int i=1;i<=n;i++)if(!id2[i]){id2[i]=++ct;for(int j=q[i];j!=i;j=q[j])id2[j]=ct;}
for(int i=1;i<=n;i++)
if(p[i]==i&&q[i]==i)as++;
else if(p[i]==i)vl[id2[i]]++;
else if(q[i]==i)vl2[id[i]]++;
else if(p[i]==q[i])adde(id[i],id2[i],1),adde(id2[i],id[i],1);
else adde(id[i],id2[i],1);
for(int i=1;i<=n*2;i++){if(vl[i])adde(n*2+1,i,vl[i]);if(vl2[i])adde(i,n*2+2,vl2[i]);}
while(bfs(n*2+1,n*2+2))as+=dfs(n*2+1,n*2+2,1e7);
printf("%d\n",n-as);
}
AGC037D Sorting a Grid
Problem
有一个 的矩阵,矩阵中 中的每个元素正好出现一次。
你需要进行如下三步操作:
- 依次考虑矩阵的每一行,对于每一行将其内部元素任意排列。
- 依次考虑矩阵的每一列,对于每一列将其内部元素任意排列。
- 依次考虑矩阵的每一行,对于每一行将其内部元素任意排列。
你需要使得最后第 行第 列的元素为 ,判断是否有解,如果有解构造方案。
Sol
考虑最后一步前合法的条件,可以发现此时合法当且仅当所有的元素都在正确的行上,即对于每个 ,第 行内的元素都在 之间。那么考虑将每个元素换为它应该在的行,这样前两步后合法当且仅当第 行上都是 。
可以发现进行了这个操作后,第一步后合法当且仅当对于矩阵的每一列,这一列上 各出现一次。因此合法当且仅当可以在第一步操作后使得每一列上 各出现一次。
首先考虑构造第一列,此时问题相当于从每一行中选出一个数(作为第一列),使得 各被选出一次。可以发现这相当于一个匹配问题,即每一行作为左侧点,每种数作为右侧点,行向这一行有的数连边,合法方案对应完美匹配。由 Hall 定理,存在完美匹配当且仅当对于左侧任意 个点,与其相邻的右侧点数量不小于 ,而在这个问题中,每行有 个数,因此 行至少有 个数,但每种数最多出现 次,因此至少有 种数,即至少有 个右侧点与这些点中的一个相邻。这说明一定存在一个完美匹配,即一定能构造出第一行。
同时可以发现,构造出第一行后,接下来的问题(构造下一行)仍然满足这个性质,因此接下来的过程一定有解,归纳可得原问题一定有解。同时可以发现之前部分完美匹配的选择不影响接下来有解的判定,因此只需要在每一次构造时任意选择一个完美匹配即可。
使用 dinic 求完美匹配,单次复杂度为 ,因此总复杂度为
Code
#include<cstdio>
#include<queue>
#include<algorithm>
#include<cstring>
#include<set>
using namespace std;
#define N 206
int head[N],cnt=1,dis[N],n,m,v[N][N],as[N][N],as2[N][N],cur[N];
multiset<int> sb[N],vl[N][N];
struct edge{int t,next,v;}ed[N*N*3];
void adde(int f,int t,int v){ed[++cnt]=(edge){t,head[f],v};head[f]=cnt;ed[++cnt]=(edge){f,head[t],0};head[t]=cnt;}
bool bfs(int s,int t)
{
queue<int> v;
memset(dis,-1,sizeof(dis));
memcpy(cur,head,sizeof(cur));
v.push(s);dis[s]=1;
while(!v.empty())
{
int r=v.front();v.pop();
for(int i=head[r];i;i=ed[i].next)
if(dis[ed[i].t]==-1&&ed[i].v)
{dis[ed[i].t]=dis[r]+1,v.push(ed[i].t);if(ed[i].t==t)return 1;}
}
return 0;
}
int dfs(int u,int t,int f)
{
if(!f||(u==t))return f;
int as=0,tp;
for(int &i=cur[u];i;i=ed[i].next)
if(ed[i].v&&dis[ed[i].t]==dis[u]+1&&(tp=dfs(ed[i].t,t,min(ed[i].v,f))))
{
ed[i].v-=tp,ed[i^1].v+=tp,as+=tp,f-=tp;
if(!f)return as;
}
return as;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&v[i][j]);
int v1=(v[i][j]-1)/m+1;
sb[i].insert(v1),vl[i][v1].insert(v[i][j]);
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n*2+2;j++)head[j]=0;
cnt=1;
for(int j=1;j<=n;j++)
{
adde(n*2+1,j,1);adde(j+n,n*2+2,1);
for(int k=1;k<=n;k++)
{
multiset<int>::iterator it=sb[j].lower_bound(k);
if(it==sb[j].end()||*it!=k)continue;
adde(j,k+n,1);
}
}
while(bfs(n*2+1,n*2+2))dfs(n*2+1,n*2+2,1e8);
for(int j=1;j<=n;j++)
for(int k=head[j];k;k=ed[k].next)
if(!ed[k].v)
{
int l=j,r=ed[k].t-n;
as[l][i]=as2[i][l]=*vl[l][r].begin();
vl[l][r].erase(vl[l][r].begin());
sb[l].erase(sb[l].find(r));
}
}
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=m;j++)
printf("%d ",as[i][j]);
for(int i=1;i<=m;i++)sort(as2[i]+1,as2[i]+n+1);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=m;j++)
printf("%d ",as2[j][i]);
}
AGC037E Reversing and Concatenating
Problem
给一个长度为 的字符串 ,你需要进行如下操作 次:
令 为 翻转后得到的字符串,令 。然后从 中选出一个长度为 的子串作为新的 。
求最后能得到的字典序最小的 。
Sol
考虑初始 中字典序最小的一种字符,设为 。
考虑一次操作,如果操作前 结尾有 个 ,则这次操作后 开头的 个 会和这一段相连,从而可以使得操作后 结尾有 个 。
那么考虑第一次操作的 中最长的连续 段,设它的长度为 ,可以发现第一次操作后一定可以使得 的结尾有 个 (如果这一段在前半段则可以选择对称位置),因此到最后一次操作时可以使得 中有连续的 个 ,而最后一次操作可以选择让这一段作为开头,从而开头有 个 。
此时如果这个值等于 ,则显然答案为 个 。考虑剩余的情况,可以发现如果一次操作前最长的连续 段长度为 ,则这次操作后最长的连续 段长度不超过 。因此上述操作一定使得最后开头的 数量最多。
同时可以发现,只有操作前在 末尾的一段在操作后可能长度变大,因此可以发现为了使最后有 个 ,一定只能在第一次选择一个长度为 的 段放在末尾,接下来每次选择 中以 开头的前 个 结尾的一段作为 ,这样才能使得每次 乘二。
因此可以发现第一次操作后,之后的操作是固定的。同时可以发现,最后得到的 首先有 个 ,接下来的部分为第一次选出的 除去结尾 后翻转,得到部分的一段前缀。因此确定第一次操作后可以 求出最后的串。
因此考虑枚举第一次操作的情况,对所有情况求得到串中最优的一个即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 10050
int n,k,tp;
char s[N],t[N],as[N],as1[N];
void solve()
{
int le=0,st=n;
while(t[st]==t[n])le++,st--;
le<<=(k-1);
if(le>n)le=n;
for(int i=1;i<=le;i++)as1[i]=t[n];
for(int i=le+1,k=st;i<=n;i++,k--)as1[i]=t[k];
for(int i=1;i<=n;i++)if(as[i]>as1[i]){for(int j=1;j<=n;j++)as[j]=as1[j];return;}
else if(as[i]<as1[i])return;
}
int main()
{
scanf("%d%d%s",&n,&k,s+1);
if(k>=14){char as='z';for(int i=1;i<=n;i++)if(s[i]<as)as=s[i];for(int i=1;i<=n;i++)printf("%c",as);return 0;}
tp=1<<k-1;
for(int i=1;i<=n;i++)s[n*2-i+1]=s[i];
as[1]='z'+1;
for(int i=n+1;i<=n*2;i++)
{
for(int j=i-n+1;j<=i;j++)t[j-i+n]=s[j];
solve();
}
printf("%s",as+1);
}
AGC037F Counting of Subarrays
Problem
给定 ,称一个序列 属于等级 ,当且仅当它满足如下两个条件之一:
- 只包含一个元素 。
- 且 可以被划分为 段,满足 且每一段都属于等级 。
给定长度为 的序列,求它有多少个子区间满足存在一个 使得这个区间属于等级 。
Sol
考虑判断一个序列是否合法,可以发现这相当于如下问题:
你可以选择连续 个相同的数 ,将其变为一个 。求是否能将这个序列变为一个数。
可以发现此时有如下结论:
- 如果存在合法方案,则一定存在一种方式使得每次选择的 单调不降。
证明:如果存在不满足这个条件的合法方案,则存在相邻两次操作使得前一次选择的 更大,这样两次操作一定互不影响,可以交换顺序。这样一直交换即可满足条件。
因此问题可以看成,每次找到序列中最小的数 ,通过一些操作将所有 变为 。考虑此时一段极长的 ,设有 个 ,则这一段可以变为 个 。
- 设初始序列中的最大值为 ,则合法当且仅当能将序列变为大于等于 个 或者序列中初始只有一个元素。
证明:如果序列中初始有多个元素,则 至少需要被合并一次。由上一结论,存在一种最优方式为从小到大合并,则合并到 时序列中只剩下 ,此时能继续合并当且仅当有 个,且有 个时下一次操作就可以变为一个 。
- 设初始序列的最大值为 ,则存在一种最优方式满足第一个条件,且在合并 的元素时,尽量使合并出的元素更多,即将长度为 的极长段合并为 个下一种元素。
证明:在满足第一种条件时,如果一个长度为 的极长段合并后元素个数小于 ,考虑将这次合并变为 个,可以发现这样只会使接下来这一段的元素个数增加。如果当前元素 ,则可以在这一段下一次合并时使用之前的数量(显然增加一个段的元素个数后之前能合并出的数量现在也可以),从而之后的操作不变。如果 ,则由上一条可知增加合并出的 数量不会变差。
因此一个序列的最优操作方式是每次选择一个最小元素的极长段,合并出尽量多的元素,直到所有元素相同。合法当且仅当过程中每次的极长段长度大于等于 且最后的元素个数大于等于 。因此 判断一个序列。
考虑上述判断的过程,这个过程也可以看成对序列建出笛卡尔树,然后在树上从下往上依次合并。这里如果一个区间内有多个最大元素,则看成它们对应同一个点,这个点有多个儿子,依次表示分出的区间。
考虑判断一个 的问题,考虑 在笛卡尔树上的 LCA,即区间内的最大值,可以发现此时相当于只保留 中元素,在笛卡尔树上合并到 LCA 处。可以发现此时对于 LCA 子树内(不考虑 LCA)的每个点,它内部保留的元素是一段前缀或者一段后缀。
考虑一个点上只保留前缀或后缀的情况,设它对应的区间内最大值为 ,则一个前缀合法当且仅当这个前缀能被合并为若干个 。考虑记录 ,其中 表示有多少个前缀通过上述过程合并可以得到 个 ,后缀同理。因为 ,每向上一层状态数至少除以二,因此总状态数为 。
考虑从下往上求出 ,可以发现求出每个子树的 后,从左向右扫一遍即可得到新的 。这部分复杂度为 。
然后考虑计算答案,考虑一个点,计算 LCA 在这个点的区间的答案。可以发现一个区间合法当且仅当内部都可以被合并上来,且能得到至少 个 。考虑枚举一个子树及 ,合法的另外一个端点一定是一段前缀的形式(前若干个子树和接下来一个子树能合并出大于等于某个个数的所有后缀,注意这里不能两个点来着同一个子树),可以预处理扫一遍得到。因此可以 计算贡献。
复杂度 。
注意这里没有计算区间只有一个点的情况,因此最后需要加上 。
注意各种细节,比如有一个子树不能合并上来的情况导致的影响。
实现时也可以不建笛卡尔树,而是按照值从小到大做上述合并并记录前后缀方向的 。
Code
from 2019.jpg
#include<cstdio>
#include<map>
#include<vector>
#include<set>
#include<algorithm>
using namespace std;
#define N 400600
map<int,int> tp1;
set<int> tp3;
struct sth{int l,r,lb,rb;friend bool operator <(sth a,sth b){return a.l<b.l;}};
vector<sth> v[N];
int su[N][2],vl[N][2],tp[N][2],ct,n,k,s[N],cnt;
long long as;
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&s[i]);
tp3.insert(s[i]);if(!tp1[s[i]])tp1[s[i]]=++cnt;
v[tp1[s[i]]].push_back((sth){i,i,1,1});
}
int lb=*tp3.lower_bound(1);
while(1)
{
int st=tp1[lb];
sort(v[st].begin(),v[st].end());
int sz=v[st].size(),las=v[st][0].l;
for(int i=0;i<sz;i++)
{
if(ct&&v[st][i].l!=v[st][i-1].r+1)
{
su[ct+1][0]=su[ct+1][1]=0;
int s1=ct/k;
if(s1)
{
for(int j=1;j<=ct;j++){int r1=j+k-1;if(r1<=ct)as+=1ll*vl[j][1]*(su[ct][0]-su[r1-1][0]);}
for(int j=1;j<s1;j++)tp[j][0]=su[(j+1)*k-1][0]-su[j*k-1][0];
tp[s1][0]=su[ct][0]-su[s1*k-1][0];
for(int j=1;j<s1;j++)tp[s1-j+1][1]=-su[ct-(j+1)*k+1][1]+su[ct-j*k+1][1];
tp[1][1]=su[ct-s1*k+1][1];
if(!tp1[lb+1])tp1[lb+1]=++cnt,tp3.insert(lb+1);
for(int j=1;j<=s1;j++)v[tp1[lb+1]].push_back((sth){las+j-1,j==s1?v[st][i-1].r:las+j-1,tp[j][0],tp[j][1]});
for(int j=1;j<=s1;j++)vl[j][0]=tp[j][0],vl[j][1]=tp[j][1],su[j][0]=su[j-1][0]+vl[j][0];
for(int j=1;j<=s1;j++){int r1=j+k-1;if(r1<=s1)as-=1ll*vl[j][1]*(su[s1][0]-su[r1-1][0]);}
}
ct=0;las=v[st][i].l;
}
vl[++ct][0]=v[st][i].lb;vl[ct][1]=v[st][i].rb;
su[ct][0]=su[ct-1][0]+vl[ct][0];su[ct][1]=su[ct-1][1]+vl[ct][1];
}
su[ct+1][0]=su[ct+1][1]=0;
int s1=ct/k;
if(s1)
{
for(int j=1;j<=ct;j++){int r1=j+k-1;if(r1<=ct)as+=1ll*vl[j][1]*(su[ct][0]-su[r1-1][0]);}
for(int j=1;j<s1;j++)tp[j][0]=su[(j+1)*k-1][0]-su[j*k-1][0];
tp[s1][0]=su[ct][0]-su[s1*k-1][0];
for(int j=1;j<s1;j++)tp[s1-j+1][1]=-su[ct-(j+1)*k+1][1]+su[ct-j*k+1][1];
tp[1][1]=su[ct-s1*k+1][1];
if(!tp1[lb+1])tp1[lb+1]=++cnt,tp3.insert(lb+1);
for(int j=1;j<=s1;j++)v[tp1[lb+1]].push_back((sth){las+j-1,j==s1?v[st][sz-1].r:las+j-1,tp[j][0],tp[j][1]});
for(int j=1;j<=s1;j++)vl[j][0]=tp[j][0],vl[j][1]=tp[j][1],su[j][0]=su[j-1][0]+vl[j][0];
for(int j=1;j<=s1;j++){int r1=j+k-1;if(r1<=s1)as-=1ll*vl[j][1]*(su[s1][0]-su[r1-1][0]);}
}
ct=0;
set<int>::iterator it=tp3.lower_bound(lb+1);
if(it==tp3.end())break;
lb=*it;
}
printf("%lld\n",as+n);
}
AGC036D Negative Cycle
Problem
有一张 个点的图,图中有以下三类有向带权边:
- 对于 ,有边 ,边权为 ,不能被删除。
- 对于 ,有边 ,边权为 ,删除的代价为 。
- 对于 ,有边 ,边权为 ,删除的代价为 。
你需要删去一些边,使得图中不存在负环且最小化删去的总代价。求出最小代价。
,所有代价非负
Sol
图中从 出发一定能到达所有点,因此没有负环时,从 出发到每个点都存在最短路。设 到 的最短路为 ,考虑 的性质:
首先由第一类边可以发现 一定单调不增。又因为每条边边权在 中,可以发现 。
考虑对于一种 ,可以保留的边。由三角形不等式可以发现能保留的边为所有满足 的边,即所有满足 且 或者 且 的边。
因为代价非负,显然应该保留所有满足条件的边。此时不难发现每个点的最短路正好是 。因此任意一种满足这个条件的 都是合法的,可以对于所有 找最小代价。
考虑计算一种 的代价。将所有 相同的点看成一个区间,则可以发现能保留的边为如下两种情况:
- 且 来自相邻区间或者同一个区间。
- 且 所属区间在 所属区间之前。
可以发现贡献只和相邻两个区间有关。设 表示考虑了 部分,最后一个区间为 时前面部分能保留的所有边代价的和的最大值。转移时枚举下一个区间,可以发现能保留的边是一个二维区间,记录二维前缀和即可 转移。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 505
long long f[N][N],su1[N][N],su2[N][N],dp[N][N],n,su;
long long Max(long long a,long long b){return a>b?a:b;}
int main()
{
scanf("%lld",&n);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(i!=j)scanf("%lld",&f[i][j]),su+=f[i][j];
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)su1[i][j]=f[i][j]*(i<j)+su1[i][j-1]+su1[i-1][j]-su1[i-1][j-1],su2[i][j]=f[i][j]*(i>j)+su2[i][j-1]+su2[i-1][j]-su2[i-1][j-1];
for(int i=1;i<=n;i++)dp[1][i]=su2[i][i];
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=j+1;k<=n;k++)dp[j+1][k]=Max(dp[j+1][k],dp[i][j]+su1[j][k]-su1[j][j]+su2[k][k]-su2[k][i-1]-su2[j][k]+su2[j][i-1]);
long long as=0;for(int i=1;i<=n;i++)as=Max(as,dp[i][n]);printf("%lld\n",su-as);
}
AGC036E ABC String
Problem
称一个包含 ABC
的字符串是好的,当且仅当它满足如下条件:
- 字符串中相邻两个字符不相同。
- 字符串中
A
,B
,C
出现次数相同。
给一个长度为 的包含 ABC
的字符串,求它的一个最长的好的子序列。
Sol
由第一条限制,连续相同字符的一段显然可以只保留一个字符。
在这一步操作后,记三种字符出现次数分别为 ,由对称性不妨设 。
此时答案长度上界为 ,考虑什么情况下能达到这个上界。此时不能删去 A
,因此可以将 A
看成分隔符,此时字符串被 A
分成了若干段,每一段都是 BC
交替。此时需要将每一段留下一部分(中间的段不能删空),使得 B
,C
出现次数等于 。此时一个段难以直接确定最优决策(例如 BC
),但可以发现如下性质:
如果 ,则一定存在长度为 的解。
证明:考虑从前往后依次删去 BC
和 CB
(如果删完满足条件一),直到满足第二个条件。如果不能继续删,此时可以发现中间每一段剩余长度不超过 ,两侧每一段长度不超过 ,因此此时 ,因此 。从而可以发现在满足第二个条件前一定不会出现不能删的情况,这样就构造出了一组解。
那么只需要让 减少到 。可以发现此时每一段的情况如下:
- 对于一个长度大于等于 的段或者一个
B
,可以通过删去两侧的C
,将这一段调整为 。可以发现不能使 更小。 - 对于一个
C
,它不能进行操作。
那么可以发现减小 的过程只需要通过删 C
做到。因此这个过程不会减小 ,不影响 最小的条件。因此可以发现存在合法方案当且仅当这样处理后可以让 ,此时可以构造出 的串,然后由上一部分即可求解。
考虑上述操作后仍然有 的情况,此时可以发现在不删 A
的情况下不可能让 ,因此只能删 A
。由上述讨论可以发现删一个 A
最多让 减小 ,且这一定可以达到:经过上述操作后,唯一会使 增加的段形如 ACA
,此时删去一对 CA
即可。那么这种情况可以再删去若干个这样的 CA
直到满足条件,然后使用上述方式构造解。上一步说明了这样最优。
因此整个上述过程可以得到最优解。两种过程都可以直接遍历一遍解决,因此复杂度 。
Code
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 1005000
int n,ci[4],is[N],ct;
char s[N],r[4]="ABC",as[N];
int main()
{
as[0]=s[0]='!';
scanf("%s",s+1);n=strlen(s+1);
int li=0;
for(int i=1;i<=n;i++)if(s[i]!=s[i-1])s[++li]=s[i];
n=li;s[n+1]=0;
for(int i=1;i<=n;i++)ci[s[i]-'A']++;
if(ci[0]>ci[1])swap(ci[0],ci[1]),swap(r[0],r[1]);
if(ci[0]>ci[2])swap(ci[0],ci[2]),swap(r[0],r[2]);
if(ci[1]>ci[2])swap(ci[1],ci[2]),swap(r[1],r[2]);
int ri=ci[2]-ci[1];
for(int i=1;i<=n;i++)if(s[i]==r[2]&&s[i-1]!=s[i+1]&&ri)ri--,is[i]=1,ci[2]--;
li=0;for(int i=1;i<=n;i++)if(!is[i])s[++li]=s[i];else is[i]=0;
n=li;
for(int i=1;i<n;i++)if(ri&&s[i]==r[0]&&s[i+1]==r[2])is[i]=is[i+1]=1,ri--,ci[0]--,ci[2]--;
li=0;for(int i=1;i<=n;i++)if(!is[i])s[++li]=s[i];else is[i]=0;
n=li;s[n+1]=0;
ri=ci[1]-ci[0];
for(int i=1;i<=n;i++)
if(ri&&s[i]*s[i+1]==r[1]*r[2]&&as[ct]!=s[i+2])ri--,i++;
else as[++ct]=s[i];
printf("%s\n",as+1);
}
AGC036F Square Constraints
Problem
给定 ,求有多少个 的排列 满足如下条件:
答案对输入整数 取模。
Sol
可以发现对于每个 , 的取值要求在一段区间内,即存在一对 ,使得限制形如 。
如果没有 的限制,则这是一个经典例子:考虑将 从小到大排序并依次确定每个 的值,这样可以发现之前可以选择的值之后一定可以选择,因此每一步选择不影响之后的选择,从而方案数为 (这里认为 单调不降)。
考虑包含 限制的情况,此时考虑对 容斥,即选择一个下标集合 ,将 内的 变为 ,计算方案数乘上 。但上述计算方案数的过程需要将所有 排序,而容斥会影响 ,也就影响了排序的过程。
但可以发现在该问题中, 具有如下性质:
- 单调不增。
- 对于 都有 。
- 。
由第二条性质,可以只容斥前半段。再由第三条性质,如果容斥后只考虑前半段的限制,则从小到大排序后一定是所有容斥的位置在前,没有容斥的位置在后,由第一条每部分内按照原先位置倒序排序,这样就容易维护顺序。
加上后半段的情况后,可以发现此时没有容斥的前半段一定在最后,前面部分为容斥的前半段和没有容斥的后半段交替。
那么可以发现一个后半段位置最后排序后的位置只和前半段容斥后比它小的位置(一段后缀)中容斥的个数有关,前半段位置如果容斥了,则位置只和前半段中它后面( 小于它)的位置中容斥个数有关,如果前半段没有容斥则位置只和它前面没有容斥的个数有关(或者和后面容斥的个数以及总共容斥的个数有关)。
因此考虑枚举总的容斥个数 ,这样求一个位置排序后所在的位置只需要前半段中一个后缀内容斥的数量。那么设 表示考虑了后 个位置,里面容斥了 个时前面部分的总方案数。转移时按照后半段的 和前半段的 的大小顺序依次考虑两侧的贡献即可。这样单次复杂度为 ,对所有 做一次即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 253
int n,p,dp[N*2][N],rs[N*2][N],lb[N*2],rb[N*2];
int main()
{
scanf("%d%d",&n,&p);
for(int i=0;i<n*2;i++)for(int j=0;j<n*2;j++)if(i*i+j*j>=n*n&&i*i+j*j<=n*n*4)
{
rb[i]=j+1;
if(!lb[i])lb[i]=j+1;
}
int l1=n,r1=n*2;
for(int i=0;i<=n;i++)dp[0][i]=(n-i)&1?p-1:1;
for(int t=0;t<n*2;t++)
{
int fg=0;
if(l1==0)fg=1;
else if(r1!=n&&(rb[r1-1]<=lb[l1-1]-1))fg=1;
if(fg)
{
r1--;
for(int i=0;i<=n*2;i++)for(int j=0;j<=n;j++)rs[i+1][j]=1ll*dp[i][j]*(rb[r1]-i)%p;
for(int i=0;i<=n*2;i++)for(int j=0;j<=n;j++)dp[i][j]=rs[i][j],rs[i][j]=0;
}
else
{
l1--;
for(int i=0;i<=n*2;i++)for(int j=0;j<=n;j++)
{
rs[i+1][j]=(rs[i+1][j]+1ll*dp[i][j]*(lb[l1]-1-i))%p;
if(j)rs[i][j-1]=(rs[i][j-1]+1ll*dp[i][j]*(rb[l1]-n*2+j))%p;
}
for(int i=0;i<=n*2;i++)for(int j=0;j<=n;j++)dp[i][j]=rs[i][j],rs[i][j]=0;
}
}
int as=0;
for(int i=0;i<=n*2;i++)as=(as+dp[i][0])%p;
printf("%d\n",as);
}
AGC035C Skolem Xor Tree
Problem
给定 ,构造一棵 个点的树满足如下条件或报告无解:
定义点 的权值为 ,对于任意 ,点 间的路径上所有点的权值异或和为 。
Sol
可以发现 无解, 时由样例可以发现一条 的链满足条件。
又因为 ,可以发现如果 是大于等于 的奇数,则可以使用如下方式构造解:
先构造 的链,然后对于每一对 ,将它们按照 的顺序挂在某一个 上。
这样解决了奇数的问题,考虑偶数的情况。考虑先用奇数的方式构造 的解,然后加入两个权值为 的叶子。注意到如果上一步所有过程挂在中间的 上,则此时这个 周围有 的所有数。因此通过这些点,可以构造出异或和为 的路径,其中 。
可以发现如果 不是 的幂,则一定有解:令 为 的二进制最高位, 取 ,不难发现这是合法的。而如果 是 的幂,则只有这两个点的权值在最高位上为 ,因此它们间的路径异或和这一位一定为 。这说明这种情况一定无解。
这样就完成了所有情况。复杂度
Code
#include<cstdio>
using namespace std;
int n;
int main()
{
scanf("%d",&n);
if(n<3){printf("No\n");return 0;}
if(n&1)
{
printf("Yes\n");
printf("%d %d\n",1,2);printf("%d %d\n",2,3);printf("%d %d\n",3,n+1);printf("%d %d\n",n+1,n+2);printf("%d %d\n",n+2,n+3);
for(int i=4;i<=n;i+=2)printf("%d %d\n",i,i+1),printf("%d %d\n",i+n,i+n+1),printf("%d %d\n",i,1),printf("%d %d\n",i+n+1,1);
}
else
{
for(int i=1;i<n;i++)
if((n^i^1)<n)
{
int st=n^i^1;
printf("Yes\n");
printf("%d %d\n",1,2);printf("%d %d\n",2,3);printf("%d %d\n",3,n+1);printf("%d %d\n",n+1,n+2);printf("%d %d\n",n+2,n+3);
for(int j=4;j<n;j+=2)printf("%d %d\n",j,j+1),printf("%d %d\n",j+n,j+n+1),printf("%d %d\n",j,1),printf("%d %d\n",j+n+1,1);
printf("%d %d\n",n,(i&1)?i+n:i);printf("%d %d\n",n*2,(st&1)?st+n:st);
return 0;
}
printf("No\n");
}
}
AGC035D Add and Remove
Problem
有 个正整数 排成一列,你需要进行如下操作 次:
选择一个不在开头结尾的数,将它两侧的数的值加上它的值,然后删去它。
你需要最小化最后留下的两个数的和,求出这个最小值。
Sol
考虑最后一个被删掉的数,设这个数的位置为 ,可以发现删去其余数后,最后的答案为 。且在删去这个数前, 两个区间内的删除情况独立(两侧都会对 贡献,但是贡献之间独立,可以分开计算)。可以发现两侧区间相当于如下两个独立问题:
- 删去 内除去端点外的数,最小化删去后 的值。
- 删去 内除去端点外的数,最小化删去后 的值。
同样地,对于这样的子问题,也可以继续枚举区间内最后一个删掉的数进行递归。由此可以得到一个如下形式的 dp:
设 表示删去 除去端点外的数,最小化最后 的值。枚举删去的最后一个位置有;(减去重复计算部分)
可以发现 只和之前每一步选择向左还是向右有关,而和每一步的下标无关,那么可以发现对于一个区间 ,到达这个区间时不同的 只有 个,因此不同状态数为 ,直接记忆化复杂度为 或 。
但这里也可以考虑不记忆化直接递归,设这样做一个长度为 的区间的复杂度为 ,则有 ,可以发现 ,即直接计算的复杂度为 ,可以发现这个数据范围下这样更快。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
int n,v[19],ct;
ll DP(int l,int r,int s1,int s2)
{
if(l+1==r)return v[l]*s1+v[r]*s2;
ll as=2e18;
for(int i=l+1;i<r;i++)as=min(as,DP(l,i,s1,s1+s2)+DP(i,r,s1+s2,s2)-v[i]*(s1+s2));
return as;
}
int main(){scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",&v[i]);printf("%lld\n",DP(1,n,1,1));}
AGC035E Develop
Problem
给定正整数 ,初始时黑板上有 间的所有整数,你可以进行任意次如下操作:
选择一个在黑板上且在 间的整数,设它为 ,接下来在黑板上擦去 ,并写上 (如果一个数已经被写上过了就不写)
记黑板上的数组成的集合为 ,求可能的 的种类数,答案对给定大整数 取模。
Sol
显然 外的整数可以不用考虑。考虑将 间的整数看成 个点,题目中的关系看成有向边(即 向 连有向边),则问题可以看成如下形式:
有一张有向图,每个点有黑白两种颜色,初始时所有点都是黑色。你每次操作可以选择一个黑色的点,将其染白,同时将它连向的所有点染黑。求能得到多少种不同的染色情况。
考虑判定染色情况的合法性,可以得到如下结论:
一个染色情况能被达到,当且仅当只保留白色节点后不存在环。
证明:首先,如果存在一个白色节点构成的环,只考虑这个环,初始时环上所有点都是黑色,而如果操作了环上一个点,则环上这个点连向的点也会变成黑色,因此无论怎么操作环上至少有一个黑色节点,这说明存在白色节点构成的环的情况不可能被达到。
另一方面,如果不存在白色节点构成的环,考虑将只保留白色节点的图进行拓扑排序,然后按照拓扑序依次操作,这样之后的操作不影响之前的操作,从而操作后就达到了这种状态。
回到原问题,此时根据不同的 ,图有不同的形式:
- 为偶数
此时奇数偶数部分完全分离,因此答案为两部分答案的乘积。
考虑一个部分,这个部分可以看成一条有向的链(),同时每个点还有向后的边()。考虑此时的一个环,环上由若干个 和 组成。环最后需要回到原点,即这些 和为 。此时不难发现一定存在一个 ,使得 环上在它之前有 个连续的 ,即链上有 个连续白点。而连续 个白点显然会造成一个环。因此此时方案合法当且仅当不存在连续 个白点。
那么这个问题可以 dp 解决;设 表示染色了前 个点,方案合法且最后一个点为黑色的方案数,转移可以前缀和优化,复杂度 。
此时可以发现情况与上一种情况中的一部分相同,使用上一部分的做法即可。
- 为奇数
此时考虑将所有 的边看成两条链,即所有奇数,偶数组成两条链。此时所有 的边为在两条链之间的边。
与上一步类似地,考虑环的性质。上一种情况中说明了如果存在环,则存在形式最简单的环。考虑此时的情况。此时环仍然可以看成由 组成,但只走一条 边不能形成环,需要两条 边。因此可以类似得到,一定存在一个 ,使得 到上一个 之前的部分和它到下一个 之前的部分中至少有 个 。考虑这样的一部分 ,可以发现只要两侧 个数之和大于等于 ,则两侧部分一定可以由某一个 形成环。
因此方案合法的条件为不存在一些白点可以组成这样一个 的环。考虑在环在某一条链的最右侧点判断,可以发现原条件等价于如下条件:
对于某一条链上的任意一个点,从这个点出发不存在正好经过一条 边且长度为 的路径。
考虑在奇数链上判断这个条件,这样只需要保留奇数链到偶数链上的边。此时如果将奇数 和 放到一排,则图形变为一个 行的网格(两侧还有额外的一些点),条件为只经过白色点,不存在一条从下面一行出发,只向左向上走到上面一行且长度为 的路径。
那么考虑从左向右进行 dp,可以发现只需要记录在当前列从下面一行出发的最长路径和从上面一行出发的最长路径。即设 表示考虑了前 列,前面部分合法且当前从这一列下面一行出发,必须走到上面的最长路径长度为 ,从上面一行出发能走的长度为 的方案数。转移时枚举这一列两个点是否是白色,然后讨论更新 即可(都不选变为 ,选下面变为 ,选上面变为 ,都选变为 )
复杂度 ,注意边界上的细节。
Code
#include<cstdio>
using namespace std;
#define N 158
int n,s,p,dp1[N],dp2[N][N][N],pw[N];
int solve(int m)
{
dp1[0]=1;
for(int i=1;i<=m;i++)
{
if(i>s/2+1)dp1[i]=(2ll*dp1[i-1]-dp1[i-s/2-2]+p)%p;
else if(i==s/2+1)dp1[i]=(2ll*dp1[i-1]-1+p)%p;
else dp1[i]=2ll*dp1[i-1]%p;
}
return dp1[m];
}
int main()
{
scanf("%d%d%d",&n,&s,&p);
pw[0]=1;for(int i=1;i<=150;i++)pw[i]=pw[i-1]*2%p;
if(~s&1)printf("%lld\n",1ll*solve(n/2)*solve(n-n/2)%p);
else
if(s==1) s=4,printf("%d\n",solve(n));
else
{
int l2=n/2,l1=n-n/2;
if(n<=s){int as=1;for(int i=1;i<=n;i++)as=as*2%p;printf("%d\n",as);return 0;}
for(int i=0;i<=(s+1)/2;i++)dp2[0][0][i]=(s+1)/2==i?1:pw[(s+1)/2-i-1];
for(int i=0;i<l1-(s+1)/2;i++)
for(int j=0;j<=s+1;j++)
for(int k=0;k<=n;k++)
if(dp2[i][j][k])
{
dp2[i+1][0][0]=(dp2[i+1][0][0]+dp2[i][j][k])%p;
dp2[i+1][0][k+1]=(dp2[i+1][0][k+1]+dp2[i][j][k])%p;
if(j<s+1)
{
dp2[i+1][j==0?0:j+1][0]=(dp2[i+1][j==0?j:j+1][0]+dp2[i][j][k])%p;
int st=j<k+1?k+2:j+1;
if(st<=s+1)dp2[i+1][st][k+1]=(dp2[i+1][st][k+1]+dp2[i][j][k])%p;
}
}
int as=0,ls=l2-(l1-(s+1)/2);
for(int i=0;i<=s+1;i++)
for(int j=0;j<=n;j++)
{
int tp=dp2[l1-(s+1)/2][i][j],s1=pw[ls];
if(ls-(s+2-i)>=0)
s1=(s1-pw[ls-(s+2-i)]+p)%p;
as=(as+1ll*tp*s1)%p;
}
printf("%d\n",as);
}
}
AGC035F Two Histograms
Problem
给定 ,有一个 行 列的网格,初始每个位置值都是 ,你可以进行如下两步操作:
- 对于每一行,选择一个 间的整数 ,将这一行前 列的格子 。
- 对于每一列,选择一个 间的整数 ,将这一列前 行的格子 。
求操作可以得到多少种不同的网格。答案模 。
Sol
设第 行加了前 个位置,第 列加了前 个位置。考虑什么情况下两组不同的 会对应到同一个矩阵。可以发现一种直接的情况是如果 ,则这种情况和 显然等价。
此时有如下结论:
- 对于一组方案,一定存在一种不存在满足 的方案,使得两种方案得到的矩阵相同。
证明:考虑每次找到一个满足 的位置改为 ,由于 在增加,这个过程只能进行有限次。
- 如果两种不同方案得到的矩阵相同,则一定有一种方案存在 。
证明:考虑最后一个满足两个方案中 不同的行,考虑两个 的最小值 对应列的位置,设这个位置为 ,则一种方案中 ,另外一种方案中 。此时考虑 这一列的情况,为了合法第一种方案一定有 ,第二种方案中一定有 ,又因为之后的行 相同,因此有 ,这样这种方案就满足条件。
因此,所有不同的网格和所有满足不存在 的方案一一对应。
考虑计算满足不存在 的方案数,考虑容斥,即钦定若干对 满足 ,可以发现如果钦定了相同的 或者相同的 则无解,否则设钦定了 对,则剩余的方案数为 。
再结合选择 的方案数,可以发现答案为:
复杂度
Code
#include<cstdio>
using namespace std;
#define N 500050
#define mod 998244353
int fr[N],ifr[N],n,m,as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
fr[0]=ifr[0]=1;for(int i=1;i<=5e5;i++)fr[i]=1ll*fr[i-1]*i%mod,ifr[i]=pw(fr[i],mod-2);
scanf("%d%d",&n,&m);
for(int i=0;i<=n&&i<=m;i++)as=(as+1ll*(i&1?mod-1:1)*fr[n]%mod*ifr[i]%mod*ifr[n-i]%mod*fr[m]%mod*ifr[m-i]%mod*pw(m+1,n-i)%mod*pw(n+1,m-i))%mod;
printf("%d\n",as);
}
AGC034D Manhattan Max Matching
Problem
在二维平面上有 个点,前 个点上有一些红球,后 个点上有一些蓝球,两种颜色的球各有 个。
你需要将红球与蓝球两两配对,定义一种配对的方式的权值为每一对球的曼哈顿距离的和(点 的曼哈顿距离为 )。
求出所有配对方式的权值的最大值。
Sol
显然有 ,即绝对值函数相当于在正负两者中取最大值。对应到曼哈顿距离上有类似的结果,即曼哈顿距离等于 取正取负, 取正取负的四种情况中和的最大值,这样对于每一种情况,贡献都可以分为只与红球位置有关的部分和只与蓝球位置有关部分的和。
可以发现这一个最大值可以和最大匹配中的最大化一起考虑,即问题变为如下形式:
你需要将红球蓝球两两配对,然后对于每一对选择如下四种状态之一,最大化贡献和。:
- 红球的贡献为 ,蓝球的贡献为 。
- 红球的贡献为 ,蓝球的贡献为 。
- 红球的贡献为 ,蓝球的贡献为 。
- 红球的贡献为 ,蓝球的贡献为 。
考虑先确定每个球的状态,确定状态后可以进行匹配当且仅当每种状态的红球蓝球数相等。因此问题可以看成,对于每个球选择一个状态,要求每种状态的红球蓝球数相等并最大化总贡献。
此时问题可以看成如下的模型:
考虑四个点表示四种状态,原点向每个红球连流量为 的边,每个红球向每种状态分别连流量为 ,费用为红球选这种状态的贡献的边,每个状态向每个蓝球连流量为 ,费用为蓝球选这种状态的贡献的边,每个蓝球向汇点连流量为 的边。
可以发现这张图上的一个最大流对应一种配对方式,因此这个图的最大费用最大流即为答案。
此时使用 primal-dual 可以做到 ,spfa 也可以通过
同时这里也可以模拟增广路的过程,考虑将四个状态对应的点看作关键点,可以发现增广路经过一个已经匹配过的球的情况相当于每一个已经匹配过的球对应一条从这四个点中的一个连向另外一个的边。此时考虑直接枚举增广路在四个点间走的方式,找到最长增广路。这样可以做到 ,这里省略细节因为我没写过
Code
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 2050
struct edge{int t,next,v,c;}ed[N*30];
int n,a,b,c,head[N],ct,cnt,vis2[N],vis3[N],vis[N];
long long dis[N],as;
void adde(int f,int t,int w,int c){ed[++cnt]=(edge){t,head[f],w,c};head[f]=cnt;ed[++cnt]=(edge){f,head[t],0,-c};head[t]=cnt;}
void bfs2(int t)
{
memset(vis2,0,sizeof(vis2));
queue<int> tp;
tp.push(t);
vis2[t]=1;
while(!tp.empty())
{
int x=tp.front();tp.pop();
for(int i=head[x];i;i=ed[i].next)
if(ed[i^1].v)
if(!vis2[ed[i].t])vis2[ed[i].t]=1,tp.push(ed[i].t);
}
}
bool spfa_is_dead(int s,int t)
{
bfs2(t);
memset(vis,0,sizeof(vis));
memset(vis3,0,sizeof(vis3));
memset(dis,0x3f,sizeof(dis));
queue<int> tp;
tp.push(s);
dis[s]=0;vis[s]=1;
while(!tp.empty())
{
int x=tp.front();tp.pop();
vis[x]=0;
for(int i=head[x];i;i=ed[i].next)
if(ed[i].v&&vis2[ed[i].t])
if(dis[ed[i].t]>dis[x]+ed[i].c)
{
dis[ed[i].t]=dis[x]+ed[i].c;
if(!vis[ed[i].t])vis[ed[i].t]=1,tp.push(ed[i].t);
}
}
return dis[t]<=1e15;
}
int dfs(int u,int t,int f)
{
if(u==t||!f)return f;
vis3[u]=1;
int as1=0,tp;
for(int i=head[u];i;i=ed[i].next)
if(!vis3[ed[i].t]&&vis2[ed[i].t]&&ed[i].v&&dis[ed[i].t]==dis[u]+ed[i].c&&(tp=dfs(ed[i].t,t,min(ed[i].v,f))))
{
as1+=tp,f-=tp;
ed[i].v-=tp,ed[i^1].v+=tp;
as+=1ll*ed[i].c*tp;
if(!f)return as1;
}
return as1;
}
long long dinic(int s,int t)
{
while(spfa_is_dead(s,t))
dfs(s,t,1e8);
return as;
}
int main()
{
scanf("%d",&n);
ct=6;cnt=1;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&a,&b,&c);
++ct;
adde(1,ct,c,0);
adde(ct,3,c,a+b);
adde(ct,4,c,a-b);
adde(ct,5,c,-a+b);
adde(ct,6,c,-a-b);
}
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&a,&b,&c);
++ct;
adde(ct,2,c,0);
adde(3,ct,c,-a-b);
adde(4,ct,c,-a+b);
adde(5,ct,c,a-b);
adde(6,ct,c,a+b);
}
printf("%lld\n",-dinic(1,2));
}
AGC034E Complete Compress
Problem
给一棵 个点的树,初始时有一些点上有一枚棋子。你可以进行如下操作:
选择两枚距离至少为 的棋子,将每一枚棋子向另外一枚棋子的方向移动一步,即找到两枚棋子间的最短路径,两枚棋子同时移动到最短路径上的下一个点。
你希望将所有棋子移动到一个点上,求出是否可行,如果可行则求出需要的最少操作次数。
Sol
猜结论真简单,证结论真阴间
考虑枚举一个点,判断能否将所有棋子移动到该点上,此时考虑将树看成以该点为根。此时可以发现如下结论:
- 存在一种最优解,使得最优解中不存在一次操作选择了一存在祖先关系的点,即不存在将一个点向下移动的操作。
证明:考虑一组存在这种操作的最优解,找到最优解中最后一次选择了一对存在祖先关系的点的操作,显然这不是最后一次操作(否则这个解不合法),设这次操作的点为 ,其中 为祖先,考虑它与下一次操作的关系:
- 如果下一次操作不涉及到 ,则可以直接交换两次操作。
- 如果下一次操作涉及到 ,设操作为 ,则可以发现操作 等价于操作 。
- 如果下一次操作涉及到 ,此时同样考虑交换两次操作,可以发现只有当 时无法进行交换,否则交换后等价。而 时可以发现,如果直接删去 操作,则下一步操作后的情况相比原先情况只是 交换了位置。而在原先操作中,下一步前 在同一个位置,因此之后交换 的编号不影响操作。从而这里将之后的 编号互换即可。
这样可以将这次操作一直向右交换,直到在某一步中被删去。可以发现删去后步数更少,矛盾。
因此对于以一个点为根的情况,如果存在解,则最优解的步数一定是所有棋子的深度和除以二,只需要判断是否存在解。同时,由这个结论,可以发现如下结论:
- 存在一种最优解,使得每次操作的两个点的 LCA 满足自下向上的关系,即不存在一次先做的操作的 LCA 是一次后做的操作的 LCA 的祖先的情况。
证明:考虑一种存在这种操作的最优解,找到两个满足条件的操作,且使得第一个操作尽量靠后,在此基础上第二个操作尽量靠前,考虑将靠前的操作依次向后移动。可以发现如果下一次操作的两个点和这一次操作的两个点有重合,则两次操作的 LCA 一定为祖先儿子关系,由于之前找的这一对的性质此时只能是第一个操作移动到了第二个操作的前一个。可以发现此时交换两个操作不影响操作过程。
考虑重复这一讨论,可以发现有限次内一定可以结束,这样就得到了满足条件的最优解。
那么操作过程可以看成从下往上考虑每个点,在每个点上只能进行 LCA 在该点的操作。这样不同子树间一定不影响。可以发现在一个子树内处理完后,之后的操作都是和子树外的点操作且是向上移动,因此只需要记录当前子树内所有棋子的深度和。
考虑在一个子树内操作结束后,子树内所有棋子的深度和。显然深度和的上限为初始的深度和,每次操作会使深度和减 。那么只需要知道最小的深度和 ,同时记初始时 子树内棋子的深度和为 ,则 子树内可能的深度和为 中所有奇偶性正确的数。
考虑从下往上求出每个点的 。首先,如果知道了当前点每个儿子内的剩余深度,则剩余深度加上儿子子树内棋子数量即为在以当前点为根时,这个子树内的所有棋子深度和,即当前该子树内可以操作的次数。此时该点上的问题相当于每个儿子子树内有一个操作次数,每次操作可以选择两个不同儿子,将这两个儿子的操作次数同时减一,最小化最后剩余的总操作次数。贪心匹配可以发现如果操作次数最多的子树的操作次数大于其余子树操作次数之和,则显然剩余最小操作次数为最大的操作次数减去其它操作次数之和,否则一定可以匹配完,即最小剩余操作次数为总操作次数模 。
回到没有确定剩余深度和的情况,首先考虑让所有子树内都取 ,考虑此时最大的子树,如果它的操作次数小于等于其它子树的操作次数和,则这样即可直接匹配。否则显然只会减小操作次数最大的子树的操作次数,可以发现如果将这个子树减小到最小(即取到 )时它的操作次数小于等于其它子树的操作次数和,则减小过程中一定有一个时刻可以达到总操作次数模 ,否则将其直接减小到最小即可求出最小剩余操作次数。这样即可求出 。
可以发现存在合法解当且仅当根节点满足 ,且由上一步有解时步数一定是所有棋子深度和除以二。对于一个点做上述过程可以在 复杂度内完成,对每个点做一次即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 2050
struct edge{int t,next;}ed[N*2];
int head[N],cnt,n,mn[N],mx[N],sz[N],a,b,as=1e9;
char is[N];
void adde(int f,int t){ed[++cnt]=(edge){t,head[f]};head[f]=cnt;ed[++cnt]=(edge){f,head[t]};head[t]=cnt;}
void dfs(int u,int fa)
{
sz[u]=(is[u]=='1');mn[u]=mx[u]=0;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs(ed[i].t,u),mx[u]+=mx[ed[i].t]+sz[ed[i].t],sz[u]+=sz[ed[i].t];
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)mn[u]=max(mn[u],mn[ed[i].t]+mx[ed[i].t]-mx[u]+sz[ed[i].t]*2);
}
int main()
{
scanf("%d%s",&n,is+1);
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),adde(a,b);
for(int i=1;i<=n;i++)dfs(i,0),as=min(as,(mn[i]||(mx[i]&1))?1000000000:mx[i]/2);
printf("%d\n",as>1e8?-1:as);
}
AGC034F RNG and XOR
Problem
给定 ,有一个能生成 间整数的随机数生成器,它的生成概率由如下过程确定:
给定一个正整数序列 ,对于每个 ,随机数生成器生成 的概率为 。每次生成之间互相独立。
你有一个数 ,初始 ,你会重复进行如下操作:用随机数生成器生成一个 ,然后将 变为 。
对于每个 ,求出 第一次变为 期望时间。答案模 。
Sol
由异或的对称性,可以发现从 开始第一次到达 的期望时间等于从 开始第一次到达 的时间,设这个值为 。
考虑 的转移,显然可以得到如下结果:
考虑将 也写成 的形式,对所有 的这一方程求和可以发现这里应该加上 的常数。
可以发现现在的转移形式和异或卷积相同,考虑用异或卷积的方式描述转移,即如下方式:
这里称 为一个集合幂级数。使用如下方式定义相加:
,则
使用如下方式定义卷积:
,则
对于卷积,由 fwt 或者高维 fft 可以发现存在如下变换:
定义 为:。则由 fft 的性质可以发现如下结果:。
同时这个变换是线性的,因此可以发现对于加法也有 。
考虑原问题,可以发现 的转移形如(记 ):
写出卷积有:
则只要 ,就能解出 。而 中所有元素为正数且和为 ,可以发现变换后只有 ,其余位置小于 ,且因为分母不超过 ,小于模数,因此其余位置模 的结果都可以解出。
现在只有 还未知,但注意到还有一个条件 。根据 fft 的理论可以发现这里的逆变换为:,即直接再变换一次后除以 。因此可以发现 等价于 ,这样即可解出所有 ,最后使用IFWT还原即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 270001
#define mod 998244353
int a[N],b[N],n,su;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
void fwt(int *a,int n,int f)
{
for(int l=2;l<=n;l<<=1)
for(int j=0;j<n;j+=l)
for(int k=j;k<j+(l>>1);k++)
{
int t1=a[k],t2=a[k+(l>>1)];
a[k]=(t1+t2)%mod;
a[k+(l>>1)]=(t1-t2+mod)%mod;
}
int ir=f?1:pw(n,mod-2);
for(int i=0;i<n;i++)a[i]=1ll*a[i]*ir%mod;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<1<<n;i++)scanf("%d",&a[i]),su+=a[i];
for(int i=0;i<1<<n;i++)a[i]=1ll*a[i]*pw(su,mod-2)%mod,b[i]=i?1:mod+1-(1<<n);
fwt(a,1<<n,1);fwt(b,1<<n,1);
for(int i=0;i<1<<n;i++)a[i]=1ll*b[i]*pw(mod+1-a[i],mod-2)%mod,a[0]=(a[0]+mod-a[i])%mod;
fwt(a,1<<n,0);
for(int i=0;i<1<<n;i++)printf("%d\n",a[i]);
}
AGC033D Complexity
Problem
给定一个 的网格,网格中每个格子为黑色或者白色。使用如下方式定义一个黑白网格的 :
- 如果一个网格只包含一种颜色,则该网格的 为 。
- 否则,考虑按照横向或者纵向将网格划分成两部分,令 为划分出的两个网格的 的最大值加一的结果,则该网格的 为所有划分方式的 的最小值。
求出给定网格的 。
Sol
这里使用 表示第 到 行, 到 列的一个子矩形。
注意到划分后得到的网格是原网格的子矩形,因此直接的方式是设 表示网格 的 值,但这样的状态数已经是 ,不可接受。
注意到如果每次直接切成尽量接近两半,则 次后一定可以全部切成 的网格,从而可以发现网格的 不超过 。
同时可以发现,如果网格 完全包含网格 ,则 的 一定大于等于 的 。
证明:考虑对 的大小归纳,找到计算 时最优的划分方式,沿着这条划分的线对 划分,对两部分分别使用归纳假设即可得证。(如果划分的线直接不经过 也容易得证)
那么可以发现,如果确定了子矩形的三条边,则子矩形的 随着第四条边向外移动而增加,但不超过 ,因此考虑记录分界点。
具体来说,考虑设 表示最大的 使得子矩形 的 不超过 ,这样的状态数为 ,且可以表示所有的 。
可以发现这样也容易处理只切纵向的转移,考虑从 到 且只切纵向的部分,由之前的性质发现可以贪心让第一段尽量长,因此不难得到此时的 。
但还需要处理切横向的转移,因此考虑再设 表示最大的 使得子矩形 的 不超过 ,这样可以处理横向的情况。
因此可以通过 时的 求出 时第一步只切纵向时的 和第一步只切横向时的 ,而 都可以表示所有的 ,因此可以用 共同得到这一步可以任意操作时的 。具体来说,从 得到 时相当于 等于最大的 使得 ,此时使用 互推相当于将每一个 和使用这种方式得到的 取 ,再对 做一次类似操作。可以发现在上述过程中,固定 后,随着 增加合法的最大 一定单调不降,因此可以双指针处理,这样即可线性地完成这一步转移,因此所有的转移都可以在状态数线性的复杂度内完成,最后第一次满足 时的 即为答案。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 187
int f[N][N][N],g[N][N][N],n,m,is[N],f2[N][N][N],g2[N][N][N];
char s[N][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
{
for(int k=1;k<=m;k++){is[k]=s[i][k]=='.';for(int l=i;l<=j;l++)if(s[l][k]!=s[i][k])is[k]=-1;}
for(int k=1;k<=m;k++)
{
if(is[k]==-1)f[i][j][k]=k-1;
else if(f[i][j][k-1]>=k)f[i][j][k]=f[i][j][k-1];
else{f[i][j][k]=k;while(f[i][j][k]<m&&is[f[i][j][k]+1]==is[k])f[i][j][k]++;}
}
}
for(int i=1;i<=m;i++)
for(int j=i;j<=m;j++)
{
for(int k=1;k<=n;k++){is[k]=s[k][i]=='.';for(int l=i;l<=j;l++)if(s[k][l]!=s[k][i])is[k]=-1;}
for(int k=1;k<=n;k++)
{
if(is[k]==-1)g[i][j][k]=k-1;
else if(g[i][j][k-1]>=k)g[i][j][k]=g[i][j][k-1];
else{g[i][j][k]=k;while(g[i][j][k]<n&&is[g[i][j][k]+1]==is[k])g[i][j][k]++;}
}
}
if(f[1][n][1]==m){printf("0\n");return 0;}
for(int i=1;i<=18;i++)
{
for(int j=1;j<=n;j++)
for(int k=j;k<=n;k++)
for(int l=1;l<=m;l++)
{
int t=f[j][k][f[j][k][l]==m?m:f[j][k][l]+1];
if(f2[j][k][l]<t)f2[j][k][l]=t;
if(g2[l][t][j]<k)g2[l][t][j]=k;
}
for(int j=1;j<=m;j++)
for(int k=j;k<=m;k++)
for(int l=1;l<=n;l++)
{
int t=g[j][k][g[j][k][l]==n?n:g[j][k][l]+1];
if(g2[j][k][l]<t)g2[j][k][l]=t;
if(f2[l][t][j]<k)f2[l][t][j]=k;
}
for(int j=1;j<=n;j++)
for(int k=n;k>=j;k--)
for(int l=1;l<=m;l++)
f2[j][k][l]=max(max(f2[j][k][l],f2[j][k+1][l]),max(f2[j-1][k][l],f2[j][k][l-1]));
for(int j=1;j<=m;j++)
for(int k=m;k>=j;k--)
for(int l=1;l<=n;l++)
g2[j][k][l]=max(max(g2[j][k][l],g2[j][k+1][l]),max(g2[j-1][k][l],g2[j][k][l-1]));
for(int j=1;j<=n;j++)
for(int k=j;k<=n;k++)
for(int l=1;l<=m;l++)
f[j][k][l]=f2[j][k][l],f2[j][k][l]=0;
for(int j=1;j<=m;j++)
for(int k=j;k<=m;k++)
for(int l=1;l<=n;l++)
g[j][k][l]=g2[j][k][l],g2[j][k][l]=0;
if(f[1][n][1]==m){printf("%d\n",i);return 0;}
}
}
AGC033E Go around a Circle
Problem
一个环被 个点划分成了 段弧。给一个长度为 ,只包含红蓝两者颜色的序列。你需要给每一段弧染红色或者蓝色,求满足如下条件的染色方案数,答案模 :
考虑从一点出发开始游走,每次走到环上相邻的一个点。对于每个出发点都存在一种走 步的方式使得经过的第 段弧的颜色等于序列中的第 个颜色。
Sol
不妨设序列中第一段为红色,则从任意位置出发,必须能在第一步走到一段红色,即每一个点必须与一段红色相邻,这相当于环上不能出现两段连续的蓝色。
考虑第一步之后的情况,如果接下来序列中不存在蓝色(即整个序列为同种颜色),则不难发现满足上一个限制的方案一定合法(对于每个点在相邻的一条红色弧上来回走即可)。这可以简单环上 dp 做到 ,以下不考虑这种情况。
否则,设 步之后下一步为蓝色,则从任意一个位置开始,可以走 步红色弧后下一步走蓝色弧,即走 步后下一步与蓝色相邻。
此时一定存在蓝色弧,因此可以看成一些蓝色弧将红色弧划分为了若干段。考虑一段内的情况,可以发现如果这一段的长度为偶数,则从第一个位置出发在段内走,考虑奇偶性可以发现走到这一段的边界点时走过的段数一定为偶数,类似的从第二个位置出发走到边界时经过的段数一定为奇数,因此无论 的奇偶性,总存在一个位置使得从这个位置开始 步后的位置不可能与蓝色弧相邻。因此合法方案中每一个极长红色弧段的长度都是奇数,相邻两段间由一个蓝色弧分隔。
如果一段的长度是奇数,则可以发现根据 的奇偶性和起始位置的奇偶性,从一个位置出发 步后应该停留在段的哪一侧是确定的,此时这一部分只需要再满足能走过去,即直接走过去的距离小于等于 。
此时如果 是奇数,则从一个边界开始,需要在 步内走到另外一个边界,可以发现这是最长的距离,因此此时的限制为每一段长度不超过 。如果 是偶数,则从一个边界开始只需要在这个边界结束,但从左边界向右一个位置开始需要到右边界,因此可以发现限制为这一段长度减一后不超过 ,即长度不超过 。可以发现因为段长度要求为奇数,因此上一种情况的限制也可以写成长度不超过 。
然后考虑接下来的过程。考虑从一个点出发的情况,由上述讨论可知序列中第一个蓝色后一定会走到一个固定的位置,且这个位置是某一个红色段的边界。考虑接下来的部分:
- 如果接下来是偶数个红色加上一个蓝色,则因为每一个红色段长度都是奇数,因此偶数个红色只能是在这一段内走然后回到原先所在的边界,再通过蓝色走到另外一个红色段的边界上,可以发现这对红色段没有任何要求。
- 如果接下来是奇数个红色加上一个蓝色,则必须走到另外一个边界,再从另外一侧的蓝色走过去,因此这会要求当前所在的红色段长度不能超过序列中这一段红色的长度。
- 最后可能有一段单独的红色,可以发现此时在红色段上任意走即可,没有其它要求。
通过这个过程可以发现,从一个点出发每个时刻在哪个段上是确定的,且容易发现从所有点出发时,在任意时刻,对于任意一个红色段,都存在一个起始点使得从它出发当前必定在这个段上(只考虑第一段后在左边界上的那些点,可以发现接下来它们的操作完全相同,即所有点一起走到下一段或者转向,因此显然每个段上有一个点),因此第二种情况中的限制会限制每一段。
综合上述讨论,可以发现如下结论:
一种染色方案合法,当且仅当它满足如下几个条件:
- 不存在两段相邻的蓝色弧。
- 记 为序列中除去第一段和最后一段(之后没有蓝色)外所有奇数长度红色段的长度的最小值与第一个红色段的长度加一的最小值,则每一个红色段的长度不超过 且为奇数。
显然可以在给定序列上 求出,考虑求方案的过程。
考虑将每一个极长的红色段和它之后的一个蓝色弧看成一个整段,可以发现此时问题相当于求将环划分成若干个长度不超过 且为偶数的方案数。
考虑破环为链,钦定包含环上第一个位置的段为第一段,剩下的段按照顺时针顺序排列,此时这个序列变回环的方案数为第一段长度。这样问题变为,将链划分为若干个满足上述条件的区间,求所有方案中第一段长度的和。考虑 dp,设 表示将长度为 的链划分为若干个满足条件的段的方案数,可以发现转移为 ,这可以前缀和优化做到 。最后考虑枚举第一段长度计算答案,可以发现答案为 ,这样就得到了答案。
复杂度
Code
#include<cstdio>
using namespace std;
#define mod 1000000007
#define N 200050
int n,m,dp[N][2][2],fg,mx,fg2,dp2[N],su[N],as;
char s[N];
int main()
{
scanf("%d%d%s",&n,&m,s+1);
for(int i=2;i<=m;i++)if(s[i]!=s[i-1])fg=1;
if(!fg)
{
dp[1][1][1]=dp[1][0][0]=1;
for(int i=2;i<=n;i++)
dp[i][0][0]=(dp[i-1][0][0]+dp[i-1][1][0])%mod,
dp[i][0][1]=(dp[i-1][0][1]+dp[i-1][1][1])%mod,
dp[i][1][0]=dp[i-1][0][0],
dp[i][1][1]=dp[i-1][0][1];
printf("%lld\n",(1ll*dp[n][0][0]+dp[n][0][1]+dp[n][1][0])%mod);
return 0;
}
int su1=0;
for(int i=1;i<=m;i++)
if(s[i]==s[1])su1++;
else
{
if((su1&1)&&mx>su1)mx=su1;
if(!fg2)mx=su1+1;
fg2=1;su1=0;
}
if(mx>n)mx=n;
if(~mx&1)mx--;
dp2[1]=1;su[1]=1;
for(int i=2;i<=n;i++)
{
if(i-mx>3)dp2[i]=(su[i-2]-su[i-mx-3]+mod)%mod;
else dp2[i]=su[i-2];
su[i]=(su[i-2]+dp2[i])%mod;
}
for(int i=1;i<=mx;i+=2)as=(as+1ll*dp2[n-i]*(i+1))%mod;
printf("%d\n",as);
}
AGC033F Adding Edges
Problem
给一棵 个点的树 ,再给一张 个点的无向图 , 使用 的点集。
重复如下过程,直到无法再进行:
选择三个不同的点 ,满足 中存在边 但不存在 ,同时 中 (以任意顺序)在某条路径上同时出现,则向 中加边 。
求出最后的边数。
Sol
reduction又一力作.jpg 早知道IOI前把这题补了
直接做极其难以优于 ,考虑从 出发,但这也很难。一个想法是考虑将加的边拆解回去,那么一条边 存在当且仅当存在一些由 中原来的边相接而成的从 到 的路径,使得路径存在一种合并方式最后合并为一条边。但可以发现合并非常困难:路径中可以通过 的方式转向(这里 在某条路径上按顺序出现),因此完全不能贪心确定合并顺序,但dp顺序就又回到了直接做。
可以发现问题就出在 的转向,等价于将 变成 ,即用更长(树中距离)的边生成更短的边。但可以发现反过来生成也是可行的,因此可以得到如下结论:
如果 中存在两条边 ,且 在 中的某条路径上顺序出现,则将 改为 后结果不变。
证明:显然在有 时 可以互相生成。
考虑不断进行上述操作直到不能继续,记势能为每条 中的边在树上距离的总和,则每次操作势能减小,因此操作一定会停止。那么停止后不再存在这种情况。此时不存在之前的转向操作,可以想象只存在向前的边,即如下结论:
在进行上述操作后,最后存在 的边当且仅当存在一条由 中的边组成的路径 ,使得每两条相邻的边都满足 在 的路径中间。
证明:首先根据加边的过程,存在当且仅当存在一条路径,满足可以每次通过操作相邻两条边(即删掉路径上一个点)的过程使得最后只剩下一条边。那么必要性是显然的:直接顺序操作过去即可。
考虑充分性,如果合并中存在转向的情况,考虑第一个转向的合并出现的位置,假设是 合并,那么在此之前都是顺序合并,即 是由一些顺序的 合并出来的, 类似。考虑这两条路径,一种情况是 的路径经过 ,此时多出来的 段都不必要,但如果不经过,考虑从 出发两条路径第一次分离的位置,此时从这个位置 连出去两条边分别到 ,但 都在 的路径上,那么这和限制矛盾。因此不存在这种情况。
因此如果完成了上述操作,则求出可以于 有边的点非常简单:从 开始在 中搜,每次只走顺序的边(即从 走到 当且仅当 上 按顺序出现,这容易 预处理 判定),那么后面这部分可以 求出答案。
那么只需要考虑如何进行操作。首先根据之前的势能,初始势能只有 ,因此可以发现最多操作 次就会停止。但此时仍然不能每修改一条边就枚举其它边看是否能继续操作。虽然很多复杂度三方的神必做法都能过
这里给出一种维护方式。考虑依次加入给定边,每次加边后操作到不能继续,再加下一条。这样每一步后都满足上述条件。考虑对于每个 维护当前 中所有与 相邻的点,将这些点放到以 为根的树 上考虑,可以发现在满足上述条件时,这些点满足在有根树上不存在一个点是另一个的祖先。
然后考虑维护操作,之前的暴力实际上在加入一条边 时只需要对两个端点分别考虑下面两步:
- 自身被其它边减小的情况,即考虑 中 的相邻点中是否存在在 中 路径上的,如果有显然只会有一个(因为满足条件),如果有 ,则将当前边变为 。
- 自身减小其它边的情况,即考虑 的相邻点是否存在在 中以 为根时 的子树内的,如果有则将这些边 变为 ,然后对这些边继续考虑。
那么相当于需要对于每个 为根的树支持如下操作:
- 查询 到根的路径上是否存在点,如果有找出该点。
- 加入 ,找出并删去 子树内的所有点。
考虑只有操作 ,那么可以每次询问时从 开始向下 dfs,搜到一个点后之后根据性质子树内就不用再考虑了。注意到加入 后这个子树之后就不会再访问了,因此每个点最多被 dfs 到 次,因此这样是均摊 的。
再考虑 操作,一个 naive 的做法是并查集维护,在之前的 dfs 时给搜到的点染色,同时删点时把子树内的合并上来。但这样多 ,还有很多多 的简单维护,都可以通过。
但可以发现把子树内合并上来也可以不做:如果一开始有 的边,但后来被 操作变为了更短的边,此时如果不合并,则 子树内的询问还会得到 。但可以发现这样也不存在问题:因为可以有 的边,那么子树内的一个 最后也可以变成 ,所以这里得到 不影响结果。这样就变为了均摊
最后是一些操作顺序的问题,因为操作可能改很多边,然后这些边会继续改。简单的实现方式是用队列维护所有还需要改的边。这里也可以直接用递归的方式进行操作,但递归需要注意顺序:例如不能先对两个端点分别检查情况 ,再对两个端点分别做情况 并分别加入,因为可能一遍做完情况 后另一边就满足情况 了,再对另一边加入情况 就会导致 AC69WA3。现在的顺序应该是对的
复杂度
Code
#include<cstdio>
#include<vector>
using namespace std;
#define N 2050
int n,m,a,b,head[N],cnt,dis[N][N],f[N][N],si[N][N],vis[N],as;
vector<int> nt[N];
struct edge{int t,next;}ed[N*2];
void adde(int f,int t)
{
ed[++cnt]=(edge){t,head[f]};head[f]=cnt;
ed[++cnt]=(edge){f,head[t]};head[t]=cnt;
}
void dfs0(int u,int fa,int fr)
{
dis[fr][u]=dis[fr][fa]+1;f[fr][u]=fa;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs0(ed[i].t,u,fr);
}
void solve(int u,int v);
void dfs1(int u,int fa,int fr,int v)
{
if(si[fr][u]){solve(v,u);return;}
si[fr][u]=v;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs1(ed[i].t,u,fr,v);
}
void solve(int u,int v)
{
if(u==v)return;
if(si[u][v]){solve(si[u][v],v);return;}dfs1(v,f[u][v],u,v);
if(si[v][u]){solve(si[v][u],u);return;}dfs1(u,f[v][u],v,u);
}
void dfs2(int u,int fa,int fr)
{
if(si[fr][u]){nt[fr].push_back(u);return;}
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs2(ed[i].t,u,fr);
}
void dfs3(int u,int fr)
{
if(vis[u])return;vis[u]=1;as++;
for(int i=0;i<nt[u].size();i++)
{
int t=nt[u][i];
if(dis[fr][u]+dis[u][t]==dis[fr][t])dfs3(t,fr);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),adde(a,b);
for(int i=1;i<=n;i++)dis[i][0]=-1,dfs0(i,0,i);
for(int i=1;i<=m;i++)scanf("%d%d",&a,&b),solve(a,b);
for(int i=1;i<=n;i++)dfs2(i,0,i);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)vis[j]=0;
dfs3(i,i);
}
printf("%d\n",(as-n)/2);
}
AGC032C Three Circuits
Problem
给一个 个点 条边的无向简单连通图,求能否用三个回路将每条边正好覆盖一次。这里的回路可以经过重复点,但不能经过重复边。
Sol
三个回路拼接后显然构成一个欧拉回路,因此有一个点度数为奇数则显然无解。
否则,考虑图的一个欧拉回路,如果有一点的度数至少为 ,则考虑从这个点将欧拉回路分成若干个环,显然可以分成至少三个环,因此此时一定有解。
否则,每个点的度数都是 或者 。如果不存在度数是 的点由图连通显然无解,接下来不考虑这种情况。考虑选择一个度数为 的点,将欧拉回路分为两部分。此时每个度数为 的点在某个欧拉回路中出现,每个度数为 的点在一个欧拉回路中出现两次,或者在两个欧拉回路中都出现。
可以发现此时有一个欧拉回路中有一个点出现了两次,则这个欧拉回路还可以再分成两个回路,这样就得到了解。剩余情况一定是每个度数为 的点都在两个欧拉回路中同时出现。但如果此时还有另外两个度数为 的点,则存在三个点 ,使得两个欧拉回路中都出现了 ,因此每个欧拉回路可以被划分为 的三条路径,将两个回路划分出的对应路径拼接即可得到三个环。这说明如果存在至少三个度数为 的点,则一定有解。
同时容易发现只存在一个度数为 的点的情况一定无解(此时图的形态容易分析),因此只需要再考虑正好有两个点度数为 的情况,记这两个点为 。
此时剩余点度数都是 ,因此这些点构成一条链。考虑从 中任一点出发,沿着链走直到到达 中一点。可以发现此时有两种情况:
- 从 出发任意一条路径都可以到达 ,即图形态为 间有四条路径相连。可以发现这种情况无解。
- 从 出发有一条路径会返回 ,可以发现此时图形态为 间有两条路径相连,还有一个经过 的环和一个经过 的环。此时存在一组解:两个经过 的环和 间的两条路径拼接得到的环。
因此此时只需要判断图属于哪种情况。一种判断方式是从 开始沿着每一条链走,看是否存在一条链能先走到 。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 100500
struct edge{int t,next;}ed[N*2];
int head[N],cnt=1,in[N],n,m,ct,mx,fg,a,b,vis[N];
void adde(int f,int t){ed[++cnt]=(edge){t,head[f]};head[f]=cnt;ed[++cnt]=(edge){f,head[t]};head[t]=cnt;in[f]++;in[t]++;}
void dfs(int u,int id)
{
vis[u]=1;
if(u==b)fg=1,vis[u]=0;
if(u==a&&id)return;
for(int i=head[u];i;i=ed[i].next)
if(i!=id&&i!=(id^1)&&!vis[ed[i].t])dfs(ed[i].t,i);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d%d",&a,&b),adde(a,b);a=-1;
for(int i=1;i<=n;i++)if(in[i]&1){printf("No\n");return 0;}else if(mx<in[i])mx=in[i];
if(mx>4){printf("Yes\n");return 0;}
for(int i=1;i<=n;i++)if(in[i]==4){ct++;if(a==-1)a=i;else b=i;}
if(ct>2){printf("Yes\n");return 0;}
if(ct==2)
for(int i=head[a];i;i=ed[i].next)
if(!vis[ed[i].t])
{
fg=0;
dfs(ed[i].t,1);
if(!fg){printf("Yes\n");return 0;}
}
printf("No\n");
}
AGC032D Rotation Sort
Problem
给一个长度为 的排列 ,给定正整数 ,你可以进行如下操作:
- 选择一个区间 ,将区间内元素向左循环位移一位,即将 变为 ,代价为 。
- 选择一个区间 ,将区间内元素向右循环位移一位,即将 变为 ,代价为 。
你需要将排列排序,求最小代价。
Sol
可以发现向左循环位移相当于将最左侧的数移动到最右侧,其余元素相对位置不变,另外一种情况同理。因此可以发现操作相当于如下形式:
- 选择一个元素,将其向前移动到前面任意一个位置,代价为 。
- 选择一个元素,将其向后移动到后面任意一个位置,代价为 。
但这样移动还是会改变中间元素的位置,这不好处理。考虑换一种方式描述移动操作,即给每个数一个绝对位置,这里绝对位置可以是实数,这样就可以在交换时不改变中间数的绝对位置。此时问题变为如下形式:
设值为 的元素的位置为 ,你可以进行如下两种操作:
- 将一个 变为比原先值小的任意实数,代价为 。
- 将一个 变为比原先值大的任意实数,代价为 。
求使 严格递增的最小代价。
此时显然每个 最多操作一次。设操作后的位置为 ,则每个位置的代价显然为如下形式:如果 则代价为 ,如果 则代价为 ,相等则没有代价。
最后的条件是 递增,因此如果从前向后 ,则状态只需要记录当前位置的 。此时 是实数,不能直接记,但不难发现因为初始位置为整数,因此对于任意一个整数 ,当前数在 中的所有情况对之后的转移是相同的。
因此可以设 表示考虑了前 个位置, 时前面的最小代价。此时直接枚举下一个数所在区间的总复杂度为 ,但可以发现转移时可以按照 分类(其中 为新的状态),每一类转移相同且与下标无关,那么扫一遍记录前面的最小值即可优化复杂度。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 5005
long long dp[N][N];
int n,a,b,p[N];
int main()
{
scanf("%d%d%d",&n,&a,&b);
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j]=1e17;
dp[0][0]=0;
for(int i=1;i<=n;i++)scanf("%d",&p[i]);
for(int i=1;i<=n;i++)
{
long long mn=1e18;
for(int j=0;j<=n;j++)
{
long long tp=mn;
if(p[i]>j)tp+=a;
if(p[i]<j)tp+=b;
if(dp[i][j]>tp)dp[i][j]=tp;
if(mn>dp[i-1][j])mn=dp[i-1][j];
tp=mn;
if(p[i]>j)tp+=a;else tp+=b;
if(dp[i][j]>tp)dp[i][j]=tp;
}
}
long long as=1e18;
for(int i=0;i<=n;i++)if(as>dp[n][i])as=dp[n][i];
printf("%lld\n",as);
}
AGC032E Module Pairing
Problem
给定正整数 和 个 间的整数 。你需要将这些整数两两配对,使得所有对 的 的最大值最小。求出这个最小值。
Sol
不妨设 单调不降。
显然每一对的权值有两种情况: 和 。考虑如果确定了每个元素所在的对的情况(上面两类中的一类),接下来应该如何确定权值。
首先考虑第一类的部分,此时需要将所有在第一类的元素配对,使得每一对的和的最大值尽量小。此时可以发现如下结论:
最优的匹配方式为直接贪心,第一个和最后一个匹配,第二个和倒数第二个匹配,以此类推。
证明:考虑其余的匹配情况,如果存在匹配分离或交叉的两对,例如四个元素匹配了 或 ,可以发现将其换为 一定更优。那么对于一个任意的匹配方式,可以第一步使用上述调整使得 匹配,下一步使得 匹配,以此类推,这就说明了上述匹配方式一定最优。
考虑第二类的匹配,此时需要每一对匹配的元素和大于等于 且最大的元素和尽量小。考虑上一种匹配方式,可以发现上一个证明同样地可以证明这种方式使得每一对的和的最小值最大。因此这一类的最优方式仍然是这样匹配,如果这样不合法则说明无解,否则这样最优。
然后同时考虑两类匹配,可以发现如下结论:
存在一组最优解,使得这组解中存在一个 ,前 小的数使用第一种方式配对,之后的数使用第二种方式配对。
证明:考虑不满足这个条件的一对第一类匹配和第二类匹配,此时有三种情况(其余情况不可能满足大小关系),设元素为 ,则情况为:
- ,此时考虑调整为配对 ,显然第一对和变小,第二对和变大,因此两对匹配的类型不变。同时 ,因此交换后一定不会变差。
- ,同样调整为 ,可以发现 。
- ,此时可以发现调整后 。
因此对于这样的两对,调整后一定更优且使得所有选择第一类匹配的点的下标和减小,因此有限次调整后可以满足条件。
再结合之前的讨论,每一类内部的最优解只需要贪心匹配,因此一个 的问题可以 求出最优解。
此时容易发现如下结论:
考虑 的方案和 的方案,对于 方案中的每一对第一类匹配,可以找到一个 方案中的第一类匹配,使得前者权值和不大于后者权值和。对于 方案中的每一对第二类匹配,也可以找到一个 方案中的第二类匹配满足条件。
证明直接讨论即可,因此随着 减小,第一类匹配的最大权值,第二类匹配的最小最大权值同时单调不增。
那么考虑二分找到最小的 使得此时第二类匹配的最小权值大于等于 ,可以发现这个 一定是最优的,求出此时的权值即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 200500
int n,p,v[N],si;
int main()
{
scanf("%d%d",&n,&p);
for(int i=1;i<=n*2;i++)scanf("%d",&v[i]);
sort(v+1,v+n*2+1);
int lb=0,rb=n,as=n;
while(lb<=rb)
{
int mid=(lb+rb)>>1,fg=1;
for(int i=1;i<=n-mid;i++)if(v[mid*2+i]+v[n*2+1-i]<p)fg=0;
if(!fg)lb=mid+1;
else rb=mid-1,as=mid;
}
for(int i=1;i<=as;i++)si=max(si,v[i]+v[as*2+1-i]);
for(int i=1;i<=n-as;i++)si=max(si,v[as*2+i]+v[n*2+1-i]-p);
printf("%d\n",si);
}
AGC032F One Third
Problem
有一个长度为 的环,你在这个环上随机选择了 个位置,并在每个位置上切了一刀,将环分成了 部分。
现在你想在环上选择一些连续的部分,使得这些部分的长度之和与 的差距最小。记这这个最小差距为 。
求出所有位置随机选择时 的期望,答案模
Sol
可以发现求最小差距相当于选择两个切的位置,使得这两个位置在环上的距离(较小的一个)和 尽量接近。
进一步考虑这个过程,这可以看成枚举第一个位置 ,然后找另外一个切的位置使得两个位置的距离和 尽量接近。可以发现这一步相当于找一个和 最接近的,一个和 最接近的,所有的最短距离的最小值即为最小差距。(因为)
这相当于每次操作时,如果选择了位置 ,则在 画一条红线,在 分别画一条蓝线,最后最短距离等于一条红线和一条蓝线间的距离的最小值。
但这样两条蓝线不好区分,因此考虑换一个操作形式:
每次操作时,如果选择了位置 ,则在 画一条红线,在 画一条蓝线,在 画一条绿线,最短距离等于两条不同颜色间距离的最小值。
此时对于一个最短距离对应的两条线 ,考虑它们对应的两组线可以发现在环上旋转 后的位置 也对应一段最短距离。因此考虑取出长度为 的一段,这样每次操作只会在段内画一条线且这一段内可以找到最短距离。
具体来说,考虑第一次切的位置 ,取出 段考虑。此时问题相当于如下形式:
有一个长度为 的段,初始时左边界有一条红线,右边界有一条蓝线。接下来进行 次操作,每次在这一段中随机选择一个位置画一条随机颜色的线,最后求这一段中不同颜色间距离的最小值。
不妨将段长度看成 求出答案,最后将答案除以 即可。
此时这些线段将段分成了 份。设第 段的长度为 ,则 ,且不难发现每组这样的 都和一个方案一一对应,因此切段可以看成在满足 的方案中随机一个。
考虑原问题,显然有如下结论:最短距离在相邻两条线间取到。因此最短距离等于所有满足两个边界颜色不同的段的 的最小值。
那么可以发现对于线的染色方案只需要知道有多少个段满足两个边界颜色不同。设有 个段满足,首先考虑求出颜色满足这个条件的概率,即求出 个位置染红蓝绿三种颜色,第一个位置为红色最后一个位置为蓝色中间随机染色,使得有 对相邻位置颜色不同的概率。
考虑把颜色相同的缩成一段,这样之后一定剩下 段,可以发现这部分的方案数为以下两部分相乘;确定每一段的颜色,再确定每一段的长度使得总长度为 。插板可得第二步的方案数为 。考虑第一段的方案数,考虑进行 dp:设 表示填了前 个位置,满足第一个位置为红色,相邻两个位置颜色不同且最后一个位置为红/蓝/绿的方案数,那么可以 预处理后求出所有 的概率。
这里也可以把 dp 看成生成函数,即写成 ,然后单位根反演可以得到 。这里也可以尝试使用递推或者其他方式得到这种结论也完全可以不用这个结论
然后考虑第二部分,即在满足 的方案中随机一个,求 的期望。
首先根据经典结论,在满足 的方案中随机一种时, 的期望为 。
证明:设 表示最短长度大于等于 的概率,那么给每一段长度减去 可以发现 。根据线性性积分可得期望为:
可以发现最短长度和总长是线性的,且前 个 的和的期望显然为 ,因此这部分的答案为 。
这样求出了一个 的答案,对所有 求和即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 1000500
#define mod 1000000007
int n,fr[N],ifr[N],dp[N][3],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d",&n);
fr[0]=1;for(int i=1;i<=n+1;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[n+1]=pw(fr[n+1],mod-2);for(int i=n+1;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
dp[0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<3;j++)for(int k=0;k<3;k++)if(j!=k)dp[i+1][k]=(dp[i+1][k]+dp[i][j])%mod;
as=1ll*dp[n][1]*pw(n,mod-3)%mod;
for(int i=1;i<n;i++)as=(as+1ll*dp[i][1]*fr[n-1]%mod*ifr[n-i]%mod*ifr[i]%mod*fr[i-1]%mod*ifr[i])%mod;
printf("%d\n",1ll*as*pw(3,mod-n-1)%mod);
}
AGC031D A Sequence of Permutations
Problem
对于两个 阶排列 ,定义一个 阶排列 为满足如下条件的排列:
给定 ,考虑如下排列序列 :
给定 ,求出 。
Sol
将排列看成置换,定义置换复合: 满足 ,可以发现复合满足结合律但不满足交换律。
定义置换的逆: 满足 ,可以发现 ,结合上一条可以发现 。
考虑题目中的操作,可以发现 。手玩可以发现:
那么可以发现:。证明可以直接归纳(注意到 具有很好的形式,然后手算每六步的操作)。
那么只需要求出 ,再模拟出前六个 即可。置换的幂可以将置换分解为环后对于每个环处理做到线性。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 1000700
int n,k,ct,p[N],q[N],rp[N],rq[N],s[N],rs[N],vis[N],st[N],as[N],s1[N];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)scanf("%d",&p[i]);
for(int i=1;i<=n;i++)scanf("%d",&q[i]);
if(k==1){for(int i=1;i<=n;i++)printf("%d ",p[i]);return 0;}
if(k<=6)
{
for(int i=3;i<=k;i++)
{
for(int j=1;j<=n;j++)s[p[j]]=q[j],p[j]=q[j];
for(int j=1;j<=n;j++)q[j]=s[j];
}
for(int i=1;i<=n;i++)printf("%d ",q[i]);return 0;
}
for(int i=1;i<=n;i++)rp[p[i]]=i,rq[q[i]]=i;
for(int i=1;i<=n;i++)s1[i]=rp[q[p[rq[i]]]];
for(int i=3;i<=(k-1)%6+7;i++)
{
for(int j=1;j<=n;j++)s[p[j]]=q[j],p[j]=q[j];
for(int j=1;j<=n;j++)q[j]=s[j];
}
for(int i=1;i<=n;i++)if(!vis[i])
{
ct=0;st[ct=1]=i;vis[i]=1;
for(int j=s1[i];j!=i;j=s1[j])st[++ct]=j,vis[j]=1;
for(int j=1;j<=ct;j++)as[st[j]]=st[(j-2+(k-1)/6)%ct+1];
}
for(int i=1;i<=n;i++)rs[as[i]]=i;
for(int i=1;i<=n;i++)printf("%d ",rs[q[as[i]]]);
}
AGC031E Snuke the Phantom Thief
Problem
二维平面上有 个点,每个点有位置 和权值 。
你需要选择一些点,最大化选择点的权值和。有 条限制,每条限制为如下四种形式之一:
- 的点选择数量不能超过
- 的点选择数量不能超过
- 的点选择数量不能超过
- 的点选择数量不能超过
求在满足限制的条件下,选择的点权值和的最大值。
Sol
首先考虑只有第一种限制的情况,此时每一个限制相当于要求选择的 坐标第 大的位置及之后位置的坐标不能这一维小于等于 。如果考虑所有限制,则相当于对于每个 有一个限制 ,表示要求选择的 第 小的位置的这一维坐标不小于等于 。
然后考虑加入第二种限制,此时这一类限制相当于对于每个 要求选择的 第 大的位置的这一维坐标不大于等于 。但两个方向的限制难以同时处理,因此考虑枚举选择的点数 ,可以发现枚举 后,限制变为对于每一个 ,选择的 第 小的位置的这一维坐标在 之间。
此时对一个点的限制和这个点的位置顺序有关,这难以扩展到二维的情况。考虑如下形式:你需要给选出的 个点分别标号 ,使得标号为 的点的这一维坐标在 之间。可以发现随着 增加,第 小位置的坐标限制区间两个端点都单调不降。那么如果标号为 的点都满足条件且标号为 的点这一维坐标更大,则交换两个点的标号后仍然合法。因此如果一种满足这个条件的标号方式不满足这一维坐标随着标号单调不降,则一定可以通过有限次上述交换使得坐标随着标号单调不降,此时标号等于横坐标顺序,因此这种方案一定合法,即满足后一个条件的方案一定满足前一个条件。同时满足前一个条件的方案直接按照这一维坐标顺序标号即可满足后一个条件,因此两个条件等价。
回到二维的情况,同样考虑枚举 ,接下来变为如下问题:
你需要选择 个点满足如下条件,最大化权值和:
- 存在 个区间 ,满足 单调不降。你选出的点需要存在一种重新排列点的方式满足排列后第 个点的横坐标在 之间。
- 存在 个区间 ,满足 单调不降。你选出的点需要存在一种重新排列点的方式满足排列后第 个点的纵坐标在 之间。
这相当于每个点可以选择一个包含它横坐标的 区间和一个包含它纵坐标的 区间,要求每个区间正好被选择一次。那么可以发现判断是否存在解的问题可以用如下网络流模型表示:
- 每个输入的点拆成左侧右侧两个点,左侧向右侧连流量为 的边。
- 原点像每个 区间连边,流量为 。每个 区间向区间内包含的点的左侧连边。
- 每个点的右侧向包含它的 区间连边。每个 区间向汇点连边,流量为 。
可以发现这里一个大小为 的流对应一种之前问题的匹配方式,因此存在解当且仅当满流。考虑求出最大收益的问题,可以发现只需要将其转化为费用流问题,给每个点拆点后中间的边加上点权的费用,求最大费用最大流即可。
这里需要做 次最大流,点数 边数 流量 ,直接 spfa 复杂度为 但是能过,使用 primal-dual 加上 dijkstra 可以做到 。
Code
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
#define N 421
#define M 38300
int s[N][3],v[N][2],n,m,l[N][2],r[N][2],head[N],cur[N],cnt,in[N],vis[N];
char c[12];
long long dis[N],c1[N],as1,as;
struct edge{int t,next,v;long long c;}ed[M];
void adde(int f,int t,int v,long long c){ed[++cnt]=(edge){t,head[f],v,c};head[f]=cnt;ed[++cnt]=(edge){f,head[t],0,-c};head[t]=cnt;}
bool spfa(int s,int t)
{
memset(dis,-0x3f,sizeof(dis));
memcpy(cur,head,sizeof(cur));
queue<int> tp;
tp.push(s);dis[s]=0;in[s]=1;
while(!tp.empty())
{
int r=tp.front();tp.pop();in[r]=0;
for(int i=head[r];i;i=ed[i].next)
if(ed[i].v&&dis[ed[i].t]<dis[r]+ed[i].c)
{
dis[ed[i].t]=dis[r]+ed[i].c;
if(!in[ed[i].t])in[ed[i].t]=1,tp.push(ed[i].t);
}
}
return dis[t]>-1e18;
}
int dfs(int u,int t,int f)
{
if(!f||u==t)return f;
vis[u]=1;
int tp,as=0;
for(int& i=cur[u];i;i=ed[i].next)
if(!vis[ed[i].t]&&ed[i].v&&dis[ed[i].t]==dis[u]+ed[i].c&&(tp=dfs(ed[i].t,t,min(f,ed[i].v))))
{
ed[i].v-=tp;ed[i^1].v+=tp;
f-=tp,as+=tp;as1+=ed[i].c*tp;
if(!f){vis[u]=0;return as;}
}
vis[u]=0;return as;
}
int dinic(int s,int t){int tp=0;while(spfa(s,t))tp+=dfs(s,t,1e8);return tp;}
int main()
{
scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d%d%lld",&v[i][0],&v[i][1],&c1[i]);
scanf("%d",&m);for(int i=1;i<=m;i++){scanf("%s%d%d",c+1,&s[i][1],&s[i][2]);if(c[1]=='U')s[i][0]=0;if(c[1]=='D')s[i][0]=1;if(c[1]=='R')s[i][0]=2;if(c[1]=='L')s[i][0]=3;}
for(int i=1;i<=n;i++)
{
cnt=1;for(int j=1;j<=402;j++)head[j]=0;
for(int j=1;j<=i;j++)l[j][0]=1,l[j][1]=100,r[j][0]=1,r[j][1]=100;
for(int j=1;j<=m;j++)if(i>s[j][2])
{
int tp=s[j][2];
if(s[j][0]==1)
{
for(int k=1;k<=i-tp;k++)l[k][0]=max(l[k][0],s[j][1]+1);
}
if(s[j][0]==0)
{
for(int k=tp+1;k<=i;k++)l[k][1]=min(l[k][1],s[j][1]-1);
}
if(s[j][0]==3)
{
for(int k=1;k<=i-tp;k++)r[k][0]=max(r[k][0],s[j][1]+1);
}
if(s[j][0]==2)
{
for(int k=tp+1;k<=i;k++)r[k][1]=min(r[k][1],s[j][1]-1);
}
}
int s1=401,t=402;
for(int j=1;j<=i;j++)adde(s1,j,1,0);
for(int j=1;j<=i;j++)
for(int k=l[j][0];k<=l[j][1];k++)
adde(j,k+100,1,0);
for(int j=1;j<=n;j++)adde(v[j][1]+100,v[j][0]+200,1,c1[j]);
for(int j=1;j<=i;j++)
for(int k=r[j][0];k<=r[j][1];k++)
adde(k+200,j+300,1,0);
for(int j=1;j<=i;j++)adde(j+300,t,1,0);
as1=0;int tp2=dinic(s1,t);
if(tp2==i)as=max(as,as1);
}
printf("%lld\n",as);
}
AGC031F Walk on Graph
Problem
使用如下方式定义一条路径的分数:设路径经过的第 条边的边权为 ,则路径的分数为 。
给一张 个点 条边的带权无向连通图,给出奇数 ,再给出 次询问:
每次询问给出 ,求是否存在一条从 到 且分数模 与 同余的路径。
Sol
考虑倒着走路径,这样 相当于你有一个分数 ,每次经过一条边时分数变为 ,这样就可以在走的过程中计算分数,且不需要额外记录路径长度。
从简单的情况开始考虑,如果 ,则从一个点到另外一个点的路径只需要满足路径长度为奇数或偶数,没有其它限制。那么当前状态可以用两个数表示:当前分数和经过的边数的奇偶性。这只有 个状态,且每个状态只有唯一后继状态,可以 从初始状态开始遍历找到所有能到达的状态,然后 回答询问。
在一般情况下,如果所有边边权相同,则从一个点到另外一个点的路径分数只与路径长度有关,此时可能限制了长度的奇偶性,也可能没有限制,使用上一种做法判断即可。
但此时如果存在不同边权,则路径分数不只与长度有关,难以直接处理。此时图上一定存在一个点满足与其相邻的边有两条边边权不同,设这两条边权为 。
首先考虑路径的性质,可以发现如下性质:
对于一条路径,可以在不改变其权值的情况下使其经过所有点。
证明:考虑往路径上加一条边,即在路径经过一个点时,让它在某条边上来回走 次,再继续之前的路径。可以发现走了 次后,分数会变为 ,而 为质数,因此存在正整数 使得 ,这个 可以使得来回走后分数不变,从而路径分数不变。由于图连通,这样依次加入每条边即可让路径经过所有点。
现在考虑使得一条路径经过与两条边权不同的边相邻的点,此时可以在这个点上来回走若干次 而不改变边权,但同时也可以将某一次走 换为走 ,使得在来回走结束后,分数增加 。那么只需要次数取足够大,就可以通过二进制拆分的方式,使得分数可以增加 ,即可以得到模 同余的所有方案。因为 和 互质,因此这些方案走到终点时仍然对应模 同余的所有方案,即有如下性质:
如果一个点有两条相邻边边权为 ,则对于一条分数模 余 的路径,一定存在一条分数模 余 的路径。
那么考虑所有能找到的 的 ,即任意两条有公共端点的边的边权差的 ,容易证明这个值等于所有边权差的 (首先后者一定是前面部分的一个公约数,而如果前者更大,则模前者后存在两条边边权不同,这样一定存在两条相邻边边权不同),设这个值为 ,则所有边权模 同余,且可以向路径分数中任意加 。
这说明只需要找到一条分数模 时与 同余的路径即可。记这个值为 ,此时有两种情况:
- ,这说明所有边权模 同余,那么此时变为之前边权全部相同的情况。
- 不满足上一个情况,但 ,此时令 ,则所有边权模 同余,即每条边的边权可以写成 ,其中 , 对所有边相同。可以发现上一种情况可以看作类似于所有边 ,因此可以先讨论这种情况。
考虑将分数写成 的形式,其中 由之前的 部分贡献, 由 部分贡献。那么同样可以发现 部分只与经过的边数有关,考虑来回经过一条边,可以发现 变为 ,但 可以模 ,因此来回经过边的操作不改变 。因此 部分只需要和之前一样记录路径经过的边数的奇偶性。
那么此时只需要记录路径的 以及经过的边数的奇偶性 ,它能表示所有 对应且经过边数的奇偶性对应的路径。首先考虑如何判断从 到 的路径能是哪几种状态。考虑将每个点拆成六个分别对应六种状态,走一条边看成一个状态向另外一个状态连有向边。这样只有 条边,但有向图可达性难以处理。但注意到如果走一个来回, 和奇偶性都不会发生变化,这说明任何一条状态转移边都存在反向边,即图是无向图。因此这部分只需要用并查集判断连通性即可。
这样可以对于每种询问得到路径可能的状态类型,考虑判断一个状态是否能得到 ,这等价于给定 ,判断是否存在一条路径在边权为 的边上来回走,走的次数奇偶性为 ,且最后权值模 与 同余。使用之前边权相同时的做法即可。可以发现第一种情况可以类似处理。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 50050
int n,m,q,p,g,s[N][3],fa[N*6],is[2][N*20],a,b,c;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int main()
{
scanf("%d%d%d%d",&n,&m,&q,&p);g=p;
for(int i=1;i<=n*6;i++)fa[i]=i;
for(int i=1;i<=m;i++)scanf("%d%d%d",&s[i][0],&s[i][1],&s[i][2]);
for(int i=2;i<=m;i++)g=gcd(g,s[i][2]-s[i-1][2]);
if(g<0)g=-g;p=gcd(p,3*g);
int s0=0,s1=0;
while(!is[s0][s1])is[s0][s1]=1,s0^=1,s1=(s1*2+s[1][2]%g)%p;
for(int i=1;i<=m;i++)
{
int vi=(s[i][2]/g)%3,ls=s[i][0],rs=s[i][1];
for(int s=0;s<2;s++)for(int t=0;t<3;t++)
fa[finds(ls+(s*3+t)*n)]=finds(rs+((!s)*3+(3+vi-t)%3)*n);
}
while(q--)
{
scanf("%d%d%d",&b,&a,&c);
int fg=0;
for(int s=0;s<2;s++)for(int t=0;t<3;t++)if(finds(a)==finds(b+(s*3+t)*n))
if(is[s][(c+(3-t)*g)%p])fg=1;
printf("%s\n",fg?"YES":"NO");
}
}
AGC030C Coloring Torus
Problem
给定 ,你需要构造一个矩阵满足如下条件:
- 矩阵为一个方阵,设其边长为 ,则
- 矩阵中只出现 间的元素,且每种元素至少出现一次。
- 定义与一个格子相邻的格子为其上下左右的四个格子(第一行向上到最后一行,最后一行向下到第一行,列上类似,如果一个格子在四个方向中出现多次 () 则计算多次),对于任意一对 ,每一个颜色为 的格子的相邻格子中颜色为 的格子数量(多次出现算多个)相等。
Sol
构造相当于要求 。首先考虑一个 的构造,容易发现两种构造方式:
- 第 行全部填 ,或者类似形式。
- 沿着每一条对角线填,例如第 行 列填 。
然后考虑从 到 。一个想法是将每一个 的元素换为 的矩阵,即将 换为 构成的矩阵,然后按照 构造。不难发现这样替换后一定合法。这样完成了 的情况。
考虑再处理 不是 的倍数的情况。此时可以发现之前的第二种构造具有好的性质:在 的第二种构造中,考虑主对角线上的两个元素,可以发现此时的状态如下(其中 为任意元素):
1 2 X X
3 4 X X
X X 1 2
X X 3 4
那么可以发现,在这条对角线上, 的相邻格子中颜色状态相同,因此考虑将 合并为同一种颜色,可以发现合并后合法。同时可以发现对于每一条对角线上都可以这样操作。
因此对于一个 ,考虑先造 的矩阵,然后使用之前的方式合并对角线,这样可以得到 的方式。可以发现这样可以解决所有 的问题,再对于小的情况暴力 即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 1040
int n,fg,as[N][N];
int main()
{
scanf("%d",&n);
if(n<=9)
{
printf("%d\n",n);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=n;j++)
printf("%d ",(i+j-2)%n+1);
return 0;
}
if(n%4)fg=4-n%4,n+=fg;
n/=2;
for(int i=1;i<=n/2;i++)
for(int j=1;j<=n/2;j++)
{
int st=(i+j-2)%(n/2)+1;
as[i*2-1][j*2-1]=st*4-3;
as[i*2-1][j*2]=st*4-2;
as[i*2][j*2-1]=st*4-1;
as[i*2][j*2]=st*4;
}
if(fg)
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(as[i][j]>3)as[i][j]--;
else if(as[i][j]==3)as[i][j]=2;
}
fg--;
}
if(fg)
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(as[i][j]>6)as[i][j]--;
else if(as[i][j]==6)as[i][j]=5;
}
fg--;
}
if(fg)
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(as[i][j]>9)as[i][j]--;
else if(as[i][j]==9)as[i][j]=8;
}
fg--;
}
printf("%d\n",n);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=n;j++)
printf("%d ",as[i][j]);
}
AGC030D Inversion Sum
Problem
给定一个长度为 的序列 。给出长度为 的序列 ,接下来进行 次操作:
第 次操作有可能不进行操作,有可能交换 。
有 种可能的操作方式,对于这些方式求和 次操作后序列 的逆序对数量。答案模 。
Sol
考虑将操作看成每次随机以 的概率交换,以 的概率不交换。那么问题相当于求逆序对数期望乘 。
那么考虑计算逆序对数的期望。序列的逆序对数难以快速维护,但逆序对数可以拆成求和的形式:对于一个序列 ,它的逆序对数等于 。
那么设 表示 的概率,则根据期望线性性,逆序对数的期望为 。
的初值可以直接计算,考虑一次操作 后 的变化,此时有如下情况:
- 两个位置都可能被交换,即 。有 的概率交换这一对,因此操作后 。
- 两个位置中有一个被交换。一种情况是 ,可以发现此时 。另外四个方向 同理。
- 两个位置都不可能被交换,此时可以发现 不变。
因此一次操作只会改变 个位置的 ,那么对每次操作直接修改 即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 3005
#define mod 1000000007
#define inv 500000004
int n,dp[N][N],v[N],q,a,b;
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)dp[i][j]=(v[i]<v[j]);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&a,&b);
int tp=dp[a][b]+dp[b][a];
for(int j=1;j<=n;j++)
{
dp[j][a]=dp[j][b]=1ll*(dp[j][a]+dp[j][b])*inv%mod;
dp[a][j]=dp[b][j]=1ll*(dp[a][j]+dp[b][j])*inv%mod;
}
dp[a][b]=dp[b][a]=1ll*tp*inv%mod;
}
int as=0;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)as=(as+dp[i][j])%mod;
for(int i=1;i<=q;i++)as=as*2%mod;
printf("%d\n",as);
}
AGC030E Less than 3
Problem
定义一个 序列是好的,当且仅当序列中不存在连续三个位置值相同。
给定两个长度为 的好的序列 。你可以对 进行修改,每次修改可以改变 一个位置的值,但要求修改后 仍然是好的。求出将 修改为 需要的最小操作次数。
Sol
考虑序列模 意义下的差分,记 ,那么可以发现一个序列是好的当且仅当 中不存在两个连续的 。
再考虑操作对差分的影响,可以发现:
- 操作开头相当于改变 。
- 操作结尾相当于改变 。
- 操作中间位置相当于选择两个相邻且不同的 并交换。
(可以发现这和 052E 几乎一样)
两个序列相等除去差分相等外,还需要某一个元素相等。考虑取第一个元素,只有第一种操作会改变 ,可以发现限制相当于改变 的操作次数奇偶性必须为某一个定值。
考虑记第 个 的位置为 。如果不考虑开头结尾的操作,显然答案下界为 。(每一步最多将这个值减少 )
可以发现这是能达到的。具体来说,按照 的大小关系,可以将序列分成若干段,对于每一段分开考虑,考虑一个 的段,可以发现在达到目标状态之前,一定有一个需要换的 可以向右换(考虑最左边一个需要换的 ,不能换的情况是 ,但此时左边的 都不需要换了而它需要向右,从而结束状态不合法,矛盾),因此每一步都能减少这个值。
而考虑开头结尾的操作后,开头将 个 变成 相当于在 的开头加入 个 (这些 初始都看成在序列左侧外)( 相当于向 开头加入 ),结尾相当于将没有配对上的看成和 配对(将 移到右侧外相当于删掉)。
这也可以看成, 两侧分别有无限个 ,选择 相当于配对为 。限制为 的奇偶性。那么这个范围下可以直接枚举 的 做到 ,也可以优化到线性。
同时,这里也可以得到类似 052E 的结论:设前 个位置中 的个数为 ,则不考虑开头操作时,需要的步数为 。考虑开头后,最小化的变为 。可以发现这个和上一种结论本质上等价。此时可以发现代价关于 显然是上凸函数,那么找到不考虑奇偶性的最优点( 的中位数),在左右找第一个合法点判断即可。
复杂度 或
Code
#include<cstdio>
using namespace std;
#define N 15005
int a[N],b[N],n,s1,s2;
char s[N],t[N];
long long solve(int t)
{
long long as=0;
int fg=0;
if(t<0){t=-t;fg=1;for(int i=1;i<=n;i++)a[i]^=b[i]^=a[i]^=b[i];s1^=s2^=s1^=s2;}
int l1=1,r1=1-t;
while(1)
{
int tp1=a[l1],tp2=r1<0?0:b[r1];
if(l1>s1)tp1=n;if(r1>s2)tp2=n;
as+=tp1<tp2?tp2-tp1:tp1-tp2;
if(tp1==tp2&&tp1==n)break;
l1++,r1++;
}
if(fg){for(int i=1;i<=n;i++)a[i]^=b[i]^=a[i]^=b[i];s1^=s2^=s1^=s2;}
return as;
}
int main()
{
scanf("%d%s%s",&n,s+1,t+1);
for(int i=1;i<=n;i++)if(s[i]!=s[i+1])a[++s1]=i;
for(int i=1;i<=n;i++)if(t[i]!=t[i+1])b[++s2]=i;
solve(-1);
long long mn=1e18;for(int i=s[1]==t[1]?-5000:-5001;i<=5000;i+=2){long long tp=solve(i);if(tp<mn)mn=tp;}
printf("%lld\n",mn);
}
AGC030F Permutation and Minimum
Problem
有一个长度为 的排列 ,现在有一些位置已经被填好了,剩余的位置还没有确定。
对于排列 ,记 ,得到一个长度为 的序列 。
剩余的位置可以任意填(需要是排列),求可能得到多少种不同的 。答案模 。
Sol
将 看成一组,那么所有的组有三种情况:两个都确定了,有一个确定了,都没有确定。
第一种组固定,可以直接去掉。第三种组间完全等价,但第二种组不等价。
考虑如何不重复的计数。在 里面的数是每一组中的较小值,因此考虑枚举哪些数是较小值,考虑此时如何计算剩余的方案数。
考虑第二种组中给定数不是较小值的那些组 ,那么这个组中必须放入一个小于 且作为较小值的数,且不同放入的方案一定对应不同的 (这个位置是确定的),那么再考虑枚举这部分匹配的情况。
考虑剩下的部分,此时有一些 需要放入一个大于 且不作为较小值的数,剩下的(没有确定的)数需要两两配对,每一对中较小的数在之前钦定作为较小值,较大的数在之前钦定作为较大值。如果只有后者,这相当于括号序列配对,因此合法当且仅当将较小值看成 ,较大值看成 ,按照元素大小排序后序列后缀和非负。在加入 后,相当于将较小值和这一类的 看作 ,较大值看作 ,然后后缀和非负。可以发现此时只要合法,无论如何匹配,最后的方案数只有 种,其中 为第三类种的个数(因为已经钦定了哪些数做较小值,此时只剩下顺序问题),那么只需要判断是否合法。
回到计数的问题,计数相当于决定每个第二类组 中的 作为较大值还是较小值,每个还没有填入的数决定作为和 配对的较小值,和另外一个没有填入的数配对的较小值或者较大值,要求考虑合法部分(作为较大值的 ,作为较大值不和 配对的较小值的未填入数( 表示第二类组中的 ))时存在合法匹配,方案数为作为较大值的 和作为较小值且和 配对的未填入数配对的方案数。
从大到小考虑,记当前状态为 ,其中 为合法部分前面剩余的匹配数量(即后缀和), 为匹配部分剩余的匹配数量。考虑到一个数时:
对于没有被填入的数 :
- 它作为和 配对的较小值,那么在匹配组中任意选择一个配对,且考虑方案数,即转移到 ,方案数为 。
- 它作为和另外一个没有填入的数配对的较小值,那么转移到 ,方案数为 (这部分不考虑方案数)
- 它作为较大值,转移到 ,方案数为 。
对于给定在第二类组中的数 :
- 它作为较小值,转移到 ,方案数为 。
- 它作为较大值,转移到 ,方案数为 。
任何时刻必须 ,最后的状态为 。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 305
#define mod 1000000007
int n,p[N*2],fg[N*2],dp[N][N],dp2[N][N],cl;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n*2;i++)scanf("%d",&p[i]);
for(int i=1;i<=n;i++)
{
if(p[i*2-1]>p[i*2])swap(p[i*2-1],p[i*2]);
if(p[i*2-1]==-1&&p[i*2]==-1)cl++;
else if(p[i*2-1]==-1)fg[p[i*2]]=1;
else fg[p[i*2-1]]=fg[p[i*2]]=2;
}
dp[0][0]=1;
for(int i=n*2;i>=1;i--)if(fg[i]==2)continue;
else if(fg[i]==1)
{
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)if(dp[i][j])
{
dp2[i][j+1]=(dp2[i][j+1]+dp[i][j])%mod;
if(i)dp2[i-1][j]=(dp2[i-1][j]+dp[i][j])%mod;
}
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j]=dp2[i][j],dp2[i][j]=0;
}
else
{
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)if(dp[i][j])
{
dp2[i+1][j]=(dp2[i+1][j]+dp[i][j])%mod;
if(i)dp2[i-1][j]=(dp2[i-1][j]+dp[i][j])%mod;
if(j)dp2[i][j-1]=(dp2[i][j-1]+1ll*dp[i][j]*j)%mod;
}
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j]=dp2[i][j],dp2[i][j]=0;
}
int as=dp[0][0];
for(int i=1;i<=cl;i++)as=1ll*as*i%mod;
printf("%d\n",as);
}
AGC029E Wandering TKHS
Problem
给定一棵 个点的树,对于每个 求出如下问题的答案:
你初始在 ,每一步你选择一个与之前访问过的点相邻,且在满足这一条件的点中编号最小的点,并访问这个点。求访问 需要的步数。
Sol
由每次走最小的形式,路径上最大的点会成为阻碍。
以 为根将树变为有根树,记点 的父亲为 。考虑 出发的问题,记 到 的路径上最大的编号为 。则 出发的过程可以分为三步:
- 重复走的过程,直到到达 。
- 走到 的父亲(因为父亲编号更小)。
- 从 开始继续这一过程。
前两部分形式都非常简单,考虑最后一部分的性质。
到 的路径上没有权值大于等于 的点,因此可以发现从 开始的过程不会经过 (选择 不比选择 到 路径上下一个点优)。那么可以发现,第三部分的过程等价于从 开始的过程。
因此设 表示从 开始的问题的答案,则 加上 前两部分的答案,即从 出发,不经过点权大于等于 的点(不考虑 的限制),能到达的点数。所有的 可以 dfs 一次求出。
那么记 表示从 出发,不经过点权大于等于 的点能到达的点数(如果 值为 )。如果 上一问题的答案即为 ,否则因为图是树,考虑枚举每一个与 相邻的点 ,答案为 之和。这样总共只需要计算 个 。
可以 Kruskal 重构树计算,也可以离线,从小到大枚举 ,并查集计算。
复杂度 或
Code
#include<cstdio>
#include<queue>
using namespace std;
#define N 200050
int fa[N],sz[N],head[N],cnt,mx[N],f[N],ti=1,as[N],n,a,b,fr[N],vl[N];
struct edge{int t,next;}ed[N*2];
priority_queue<pair<int,int> > st;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
void adde(int f,int t){ed[++cnt]=(edge){t,head[f]};head[f]=cnt;ed[++cnt]=(edge){f,head[t]};head[t]=cnt;}
void dfs1(int u,int fa){f[u]=fa;mx[u]=mx[f[u]],fr[u]=fr[f[u]];if(mx[u]<fa)mx[u]=fa,fr[u]=u;for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs1(ed[i].t,u);}
int que(int x){if(x<ti)return sz[finds(x)]-1;int as=0;for(int i=head[x];i;i=ed[i].next)if(ed[i].t<ti)as+=sz[finds(ed[i].t)];return as;}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),adde(a,b);
for(int i=1;i<=n;i++)fa[i]=i,sz[i]=1;
dfs1(1,0);
for(int i=head[1];i;i=ed[i].next)st.push(make_pair(-1,ed[i].t));
while(!st.empty())
{
int las=ti;
ti=-st.top().first;int s=st.top().second;st.pop();
for(int i=las;i<ti;i++)
for(int j=head[i];j;j=ed[j].next)if(ed[j].t<i){int a=finds(i),b=finds(ed[j].t);fa[a]=b,sz[b]+=sz[a];}
as[s]+=as[mx[s]]+que(s)+1-vl[fr[s]];
for(int i=head[s];i;i=ed[i].next)if(ed[i].t!=f[s])st.push(make_pair(-mx[ed[i].t],ed[i].t)),vl[ed[i].t]=ed[i].t<ti?sz[finds(ed[i].t)]:0;
}
for(int i=2;i<=n;i++)printf("%d ",as[i]);
}
AGC029F Construction of a Tree
Problem
给定 和 个 的子集 。
你需要在每个子集中选出两个数 ,使得对每一对 连边 后,得到的图是一棵树。
构造任意方案,或输出无解。
Sol
考虑最后得到的树,选择一个点为根,那么相当于除去根外的每个点需要对应一条边。
因此合法的必要条件是能在每个集合中选出一个点(这个点对应这个集合的边),使得选出的点两两不同。
考虑如果得到了这样一个集合和点的匹配,是否存在合法方案。
从根开始考虑,如果其它点中有一个点对应的集合包含根,那么可以让这个点连向根。
更进一步,如果一个点对应的集合包含根当前所在的连通块,那么它也可以连过来,加入根的连通块。
如果这个过程得到了解就构造出了方案。考虑不能构造出的情况,即存在一个不包含根的点集 , 中每个点对应的集合都是 的子集。
但这个时候,这 条边只能连在这 个点中,从而这不可能是树。因此这种情况一定无解。
那么求出匹配后,使用上述方式判定即可。记录每个点所属的集合后可以做到线性。
求匹配可以 dinic,因为是二分图匹配的形式复杂度为 。
Code
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define N 200500
int n,a,b,head[N],cnt=1,cur[N],dis[N],su;
struct edge{int t,next,v;}ed[N*11];
void adde(int f,int t,int v)
{
ed[++cnt]=(edge){t,head[f],v};head[f]=cnt;
ed[++cnt]=(edge){f,head[t],0};head[t]=cnt;
}
bool bfs(int s,int t)
{
for(int i=1;i<=su;i++)cur[i]=head[i],dis[i]=-1;
queue<int> qu;
qu.push(s);dis[s]=0;
while(!qu.empty())
{
int u=qu.front();qu.pop();
for(int i=head[u];i;i=ed[i].next)if(ed[i].v&&dis[ed[i].t]==-1)
{
dis[ed[i].t]=dis[u]+1;qu.push(ed[i].t);
if(t==ed[i].t)return 1;
}
}
return 0;
}
int dfs(int u,int t,int f)
{
if(u==t||!f)return f;
int as=0,tp;
for(int &i=cur[u];i;i=ed[i].next)
if(dis[ed[i].t]==dis[u]+1&&(tp=dfs(ed[i].t,t,min(f,ed[i].v))))
{
ed[i].v-=tp;ed[i^1].v+=tp;
as+=tp;f-=tp;
if(!f)return as;
}
return as;
}
int as[N][2];
queue<int> rs;
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d",&a);
while(a--)scanf("%d",&b),adde(b,i+n,1);
adde(i+n,n*2+1,1);
}
for(int i=1;i<=n;i++)adde(n*2,i,1);
su=n*2+1;
int si=0;
while(bfs(su-1,su))si+=dfs(su-1,su,n);
if(si<n-1){printf("-1\n");return 0;}
for(int i=1;i<=n;i++)
{
int fg=1;
for(int j=head[i];j;j=ed[j].next)if(ed[j].t<n*2&&!ed[j].v)
fg=0,as[ed[j].t-n][0]=i;
if(fg)rs.push(i);
}
while(!rs.empty())
{
int u=rs.front();rs.pop();
for(int i=head[u];i;i=ed[i].next)if(ed[i].t<n*2)
{
int ri=ed[i].t-n;
if(!as[ri][1])as[ri][1]=u,rs.push(as[ri][0]);
}
}
for(int i=1;i<n;i++)if(!as[i][1]){printf("-1\n");return 0;}
for(int i=1;i<n;i++)printf("%d %d\n",as[i][0],as[i][1]);
}
AGC028C Min Cost Cycle
Problem
给定 和两个长度为 的正整数序列 。现在考虑一个完全有向图,其中 的边权是 。
求出图中一个经过所有点正好一次的环的最小边权和。
Sol
题目要求中有两重最小值: 和整体最小值,那么可以将里面的 和外面合并。具体来说,问题可以变为先找一个环,然后每条边在 中任意选一个作为边权,然后最小化边权和。
可以发现每个 和每个 都在选择中正好出现一次,因此最后的选择一定是在这 个数中选 个加起来。但还需要考虑哪些选 个的方式是合法的。
考虑一种选择了 个数的方式。可以发现对于每个 , 是否选择表示它在环上是否使用下一条边, 是否选择表示它在环上是否使用上一条边。那么问题相当于,给定每个点是否使用上一条边和是否使用下一条边,求是否能将它们拼成一个合法的环。形象地考虑,用 <
表示下一个点使用这条边,>
表示上一个点使用,那么相当于有一些形如 <o>
、<o<
或者类似的点,求能否将它们合并为一个环(即环上拼图)。
先只考虑 <o>
,>o<
,可以发现这两种出现次数应该相等(总共选了 个数,这两个分别代表选了 个,另两种则是 个),那么这两种交替排即可。此时如果有剩下的 <o<
和 >o>
都可以插入到某个 >o<
两侧,那么就合法。
但如果这两种都不出现,只有 <o<
和 >o>
时,可以发现如果后两种同时出现,它们一定会直接相邻,那么不合法,但只出现一种是可以的。因此可以得到合法条件为:
- 存在一个 使得 同时被选,或者
- 选择所有的 ,或者选择所有的 。
第二类情况特殊处理。对于第一类情况,有很多种做法。直接的做法是枚举一个 ,也可以进行一些分类讨论:
考虑直接选最小的 个,如果合法就行了。否则,前 小的数中每一个 正好出现一次,此时必须至少从后 个中选一个替换过来。代价最小的方式是拿第 个换掉第 个,可以发现除非 来自同一个 ,否则这样换都是合法的。但如果这样不行,考虑换 或者 ,由于 来自同一种,那么后面两种交换都合法。而可以发现这两种交换中一定有一种是除去 后最优的(即使考虑换多个数),因此只考虑这两种情况即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 200590
#define ll long long
int n,a[N],b[N],vis[N],fg1,fg2;
ll v[N],s1,s2,as;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]),s1+=a[i],s2+=b[i],v[i]=1ll*a[i]*(n+1)+i,v[i+n]=1ll*b[i]*(n+1)+i;
sort(v+1,v+n*2+1);
for(int i=1;i<=n;i++)as+=v[i]/(n+1),fg1|=vis[v[i]%(n+1)],vis[v[i]%(n+1)]=1;
if(!fg1)
if(v[n]%(n+1)!=v[n+1]%(n+1))as+=v[n+1]/(n+1)-v[n]/(n+1);
else as+=min(v[n+2]/(n+1)-v[n]/(n+1),v[n+1]/(n+1)-v[n-1]/(n+1));
printf("%lld\n",min(as,min(s1,s2)));
}
AGC028D Chords
Problem
在圆环上有 个点顺序排列。
考虑将这些点配对成 对,在每一对间连边。定义一种方案的权值为连出的这些边构成的连通块数量(相交即为连通)。
现在有 对已经给定,剩余 个点间可以任意配对,求此时所有可能方案的权值和。答案模 。
Sol
首先考虑如何数连通块。常见的方式是考虑代表元。例如可以考虑在连通块中标号最小的元素处统计,但此时从 出发不能到达小于 的点这一限制仍然难以直接处理,需要考虑 能走到哪。另一种考虑是转而找一条线分开圆环且不与连边相交,但这样如果边是 则显然会算重,可以发现至少需要让一边的两端连通,但这样还有重复。
可以发现以上两种思路最后都指向一种方式:考虑记录连通块中编号最小的点 和最大的点 ,则要求 连通,且它们和剩余部分相邻。那么考虑设 表示 内进行配对,使得 连通的方案数。
考虑如何进行转移。连通难以直接处理,因此考虑容斥,计算不连通的方案数:记 表示 内配对的方案数,枚举实际上 所在连通块中的最大点 ,可以发现转移为:
最后的答案也可以类似处理:枚举每一对 ,答案是 乘上另一部分内部配对的方案数。
最后考虑如何算 ,问题在于有给定的限制。可以发现如果给定的限制使得不可能内部连边(存在 内部连向外部的),那么 ,否则只需要内部还没有被配对的点任意配对,即 个点任意配对。显然方案数为 (每次取剩余编号最小的出来配对)
另一种实现方式是,如果 不满足条件,就把 设为 而不是把 设为 ,从而不显式维护 。这样也是对的,因为在合法情况中,内部配对时如果一侧不满足条件,那么另一侧自动不满足。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 605
#define mod 1000000007
int dp[N][N],n,m,a,b,t[N],s[N],su[N],as;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d%d",&a,&b),t[a]=b,t[b]=a;
s[0]=1;for(int i=2;i<=n*2;i++)s[i]=1ll*s[i-2]*(i-1)%mod;
for(int i=1;i<=n*2;i++)su[i]=su[i-1]+!t[i];
for(int l=1;l<=n*2;l++)
for(int i=1;i+l<=n*2;i++)
{
int j=i+l;
int fg=1;
for(int k=i;k<=j;k++)if((t[k]<i||t[k]>j)&&t[k])fg=0;
if((su[j]^su[i-1])&1)fg=0;
if(!fg)continue;
int tp=s[su[j]-su[i-1]];
for(int k=i;k<j;k++)tp=(tp-1ll*dp[i][k]*s[su[j]-su[k]]%mod+mod)%mod;
dp[i][j]=tp;as=(as+1ll*tp*s[su[n*2]-su[j]+su[i-1]])%mod;
}
printf("%d\n",as);
}
AGC028E High Elements
Problem
定义一个序列的权值为前缀最大值的个数。
给一个长度为 的排列,你需要将其划分为两个子序列,使得两个子序列的权值相等。
更进一步,每种方案可以用一个长度为 的 序列 表示: 当且仅当排列第 个位置被划分到了第一个子序列。如果有解,则你需要找到合法解中使得 字典序最小的解。
Sol
首先考虑怎么判合法。显然原排列中的前缀最大值一定是子序列中的前缀最大值。而原先的非前缀最大值一定可以不是子序列的前缀最大值:只需要放到本来覆盖它的前缀最大值处即可,但它们也可以变成子序列的前缀最大值。
那么可以发现,如果原序列的前缀最大值个数为偶数,则有显然的解:直接分原来的前缀最大值即可。
考虑原序列有奇数个前缀最大值的情况。此时必定有一个非原序列前缀最大值的元素变为了划分后子序列的前缀最大值。此时方案可能是复杂的:两个序列可以轮流出现新的前缀最大值,交替难以处理。但注意到新出现的前缀最大值放到另一个序列后就不产生影响了,因此如果两个序列都有新出现的前缀最大值,可以两边各选一个放回去,这样调整后可以得到如下性质:
如果有解,则存在一组解,其中一个子序列的前缀最大值都是原序列的前缀最大值。
那么只需要考虑另一个子序列。因为第一个子序列中只出现原序列的前缀最大值,因此确定了第二个子序列的前缀最大值后即可确定第一个子序列的:因为原前缀最大值也是新前缀最大值,因此第一个子序列的前缀最大值即为不在第二个子序列中作为前缀最大值出现的所有原序列前缀最大值。
考虑第二个子序列的前缀最大值的结构。首先这些前缀最大值显然构成一个上升子序列,然后可以发现这是充分必要的:考虑那些插入后会破坏当前前缀最大值的元素,如果它是原序列前缀最大值,那么它显然在第一个子序列。而如果它不是,原序列中它左侧的前缀最大值在它左侧且比它大,因此它左侧的第一个原序列前缀最大值也会破坏第二个子序列的前缀最大值,从而该元素在第一个子序列,这样当前元素放入第一个子序列也不会有问题。
那么问题变为:能否找到一个上升子序列,使得其长度等于不在其中出现的原序列前缀最大值数量。可以发现偶数个前缀最大值的情况也可以被该问题解决,那么之后可以不再考虑第一步的奇偶性。
问题也可以看成,选原序列前缀最大值有 的权值(同时去掉了一个第一个序列的),选其余的有 的权值,求能否找到一个上升子序列使其权值和为原先前缀最大值的数量。
显然不能直接维护可能出现的分数。但可以发现,上升子序列能任意删元素,而权值只有 ,因此如果能做到 的权值,则也能做到 的权值。因此只需要记录奇数偶数分别的最大值。因此考虑设 表示结尾的数在第 个及之前且不超过 ( 值原序列)时,权值和为奇数和偶数时分别的最大值(这样定义是因为可能删到 的时候必须删 ,但对分析不产生影响)。转移可以从前向后扫处理下标维,对值域维 BIT,分别维护奇偶的最大值。最后合法当且仅当存在一个结尾的状态满足奇偶性且大于等于需要的值。那么可以 判定。
现在考虑字典序最小。显然的想法是逐位确定,那么需要解决确定了一个前缀后,后面是否合法的问题。那么考虑和上面类似的分析过程。首先可以类似得到,在后缀部分一定有一个序列只用了原序列的前缀最大值。考虑枚举哪个序列接下来可以用非前缀最大值,那么类似的定义权值,相当于在后缀且权值大于某个值的部分选一个上升子序列,使其权值等于另一个权值之前的前缀最大值数量加上后缀在原序列中的前缀最大值数量再减去当前序列之前的前缀最大值数量。那么考虑把之前的 dp 倒过来做,这样就能支持后缀询问。简单的实现是向后扫时删掉之前的状态(相当于撤销加入),然后仍然只剩值域维,把 BIT 改成简单的线段树即可。一个细节是这里不仅要判定是否上界大于等于需要的权值,还需要处理需要的权值为负数的情况:此时显然无解。那么判定一个前缀方案是否合法可以 (在记录两个序列最大值和前缀最大值数量的情况下)
复杂度
fun fact: 下面的代码在让第二个序列接可以取非原序列前缀最大值时,没判权值必须非负,但是过了。可能的解释是因为贪心取字典序最小,那么第一个序列的前缀最大值数量很多情况下比第二个多。但我不会证。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 200500
int n,p[N],dp[N][2],mx,is[N];
struct segt{
int tr[N*3];
void init(){for(int i=1;i<(1<<19);i++)tr[i]=-1e9-1;}
void modify(int x,int v)
{
int nw=x+(1<<18);
tr[nw]=v;
while(nw)nw=nw>>1,tr[nw]=max(tr[nw<<1],tr[nw<<1|1]);
}
int query(int x)
{
int nw=x+(1<<18),as=tr[nw];
while(nw)
{
if(~nw&1)as=max(as,tr[nw+1]);
nw>>=1;
}
return as;
}
}tr[2];
int l1,l2,v1,v2,ri;
bool chk(int l1,int l2,int v1,int v2,int ri)
{
int vl=tr[(v1+v2+ri)&1].query(l2);
if(v2+vl>=v1+ri)return 1;
vl=tr[(v1+v2+ri)&1].query(l1);
return v1+vl>=v2+ri&&v2+ri>=v1;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&p[i]);
for(int i=1;i<=n;i++)mx=max(mx,p[i]),is[i]=p[i]==mx,ri+=is[i];
tr[1].init();
for(int i=n;i>=1;i--)
{
dp[i][0]=tr[0].query(p[i]);if(dp[i][0]<0)dp[i][0]=0;
dp[i][1]=tr[1].query(p[i]);
if(is[i])dp[i][0]+=2,dp[i][1]+=2;
else swap(dp[i][0],dp[i][1]),dp[i][0]++,dp[i][1]++;
tr[0].modify(p[i],dp[i][0]),tr[1].modify(p[i],dp[i][1]);
}
for(int i=1;i<=n;i++)
{
ri-=is[i];
tr[0].modify(p[i],0);tr[1].modify(p[i],0);
if(chk(max(l1,p[i]),l2,v1+(l1<p[i]),v2,ri))
printf("0"),v1+=l1<p[i],l1=max(l1,p[i]);
else
{
if(i==1){printf("-1\n");return 0;}
printf("1"),v2+=l2<p[i],l2=max(l2,p[i]);
}
}
}
AGC028F Reachable Cells
Problem
给一个 的矩阵,每个位置可能是障碍 #
,也可能是空位。每个空位有 间的一个整数权值。
称一个位置能到达另一个位置,当且仅当这两个位置都不是障碍,且只通过向右和向下走,能从前一个位置在不经过障碍的情况下走到后者。
现在考虑所有满足 且 能到达 的位置对 ,对所有对求和 位置上权值的乘积。
Sol
考虑求一个点能到达哪些点,naive的想法是找左边界和右边界,但容易发现两个边界中的点不一定能到达:
S....
.###.
.#T#.
.....
考虑进行一些 fix,注意到这是一个平面上的路径,那么如果两条路径相交,则可以互换终点。由此可以发现如下结论:
如果 能到达 所在的行,且 能到达位置的左右边界覆盖了 (实际上这包含上一个限制),且 可以反向到达 所在的行,则 能到达 ,且这是充分必要的。
证明:如果 能到达 ,则上述限制显然被满足。考虑另一方向,设 能反向到达 所在行的另一个位置,如果这个位置在 左侧,则 到左边界的路径和这个位置到 的路径交叉,因此必然相交,从而 能到达 ,如果在右侧则可以使用右边界的路径。
那么考虑枚举每一行,如果能算出这一行每个点往下的左右边界(对于一行是 个数),再预处理每个点反向最多能上到多少行,就可以 算这一行的贡献。
考虑如何求左右边界。一种想法是通过当前行推下一行的边界。可以发现在固定了起始行后,推到下一行时只有一些位置能转移,即满足从第 行能到达,且下面一个位置也是空位的当前行位置,可以通过预处理的内容求出哪些位置合法。每个位置走到下一行后在下一行内能到达的点也容易求出,显然随着位置递增,在下一行能到达部分的左右端点都单调不降。那么转移只需要找到上一行的可达区间中的第一个和最后一个合法转移点。对这一行预处理后即可 回答。
那么可以在 内对于一个起始行,算出这一行每个位置出发的左右边界。算贡献可以每一个起始行做一次前缀和。
复杂度
但这个做法常数很大,不能直接过 F2。一些小常数做法能直接三方过 F2。
Code
#include<cstdio>
using namespace std;
#define N 505
int n,rv[N][N],lb[N][N],rb[N][N],su[N],r1[N][N],dep[N][N],vl[N];
char s[N][N];
long long as;
void dfs(int x,int y)
{
if(x<n&&s[x+1][y]!='#'&&!rv[x+1][y])rv[x+1][y]=rv[x][y],dfs(x+1,y);
if(y<n&&s[x][y+1]!='#'&&!rv[x][y+1])rv[x][y+1]=rv[x][y],dfs(x,y+1);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%s",s[i]+1);
for(int i=n;i>=1;i--)for(int j=n;j>=1;j--)if(s[i][j]!='#')
{
dep[i][j]=i;
if(dep[i+1][j]>dep[i][j])dep[i][j]=dep[i+1][j];
if(dep[i][j+1]>dep[i][j])dep[i][j]=dep[i][j+1];
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(s[i][j]!='#'&&!rv[i][j])rv[i][j]=i,dfs(i,j);
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)r1[i][j]=rv[i][j];
for(int f=1;f<=n;f++)
{
for(int i=1;i<=n;i++)lb[f][i]=rb[f][i]=i;
for(int i=n-1;i>=1;i--)if(s[f][i]!='#'&&s[f][i+1]!='#')rb[f][i]=rb[f][i+1];
for(int i=f+1;i<=n;i++)
{
vl[n+1]=n+1;
for(int j=n;j>=1;j--)
if(rv[i][j]&&rv[i][j]<=f&&s[i-1][j]!='#')vl[j]=j; else vl[j]=vl[j+1];
for(int j=1;j<=n;j++)lb[i][j]=vl[lb[i-1][j]];
for(int j=1;j<=n;j++)if(rv[i][j]&&rv[i][j]<=f&&s[i-1][j]!='#')vl[j]=j; else vl[j]=vl[j-1];
for(int j=1;j<=n;j++)rb[i][j]=vl[rb[i-1][j]];
vl[n]=n;
for(int j=n-1;j>=1;j--)if(s[i][j]!='#'&&s[i][j+1]!='#')vl[j]=vl[j+1]; else vl[j]=j;
for(int j=1;j<=n;j++)rb[i][j]=vl[rb[i][j]];
for(int j=1;j<=n;j++)
{
su[j]=su[j-1];
if(s[i][j]!='#'&&rv[i][j]<=f)su[j]+=s[i][j]-'0';
}
for(int j=1;j<=n;j++)if(lb[i][j]<=rb[i][j]&&dep[f][j]>=i)
as+=(s[f][j]-'0')*(su[rb[i][j]]-su[lb[i][j]-1]);
}
}
for(int i=1;i<=n;i++)
{
int su=0;
for(int j=1;j<=n;j++)if(s[i][j]=='#')su=0;
else as+=su*(s[i][j]-'0'),su+=s[i][j]-'0';
}
printf("%lld\n",as);
}
AGC028F2 Reachable Cells
Problem
同上一个题。
Sol
仍然考虑算出每个点能到达的点的权值和,但此时无法再对于每一行都算出左右边界:这样边界的总大小是 ,同时从下一行求上一行的过程需要合并若干个边界。
对于难以直接维护每一行开始的答案的情况,可以考虑分治:将矩阵划分为上下两部分,求出上一部分到下一部分的所有答案。以分界线为中转。此时需要的信息只有分界线上每个点向下的左右边界以及每个上半部分的点能到哪些分界线上的点。
然后考虑算上半部分一个点在下面的答案。可以发现这应该是它能到达的所有分界线上的点向下能到的点的并,即对一些点集求并。但直接求并显然是困难的,因此仍然需要考虑平面向右向下游走的性质。
一种思路是仍然类似F1的做法,维护左右边界。考虑枚举起始行,删去不能反向到达起始行的分界点,那么此时能到的分界点构成一段区间。注意到根据相交的性质,容易发现如果左右两个点都能到同一行,那么它们在这一行能到的区间两端都是不降的。因此求左右边界可以使用如下方式:依次考虑下面的每一行,删去不能到这一行的分界点,然后取剩余的首尾两个的左右边界作为区间。那么这相当于左边界从某个分界点的左边界开始向下走,到这个左边界终止时移动到右侧第一个还存在的左边界继续,直到左右边界重合。但求左右边界需要最后在范围内只保留反向能到起始行的点(F1的要求)。
那么可以把一行的所有询问一起做,权值合并,这样总共 次边界移动的过程,即 段边界。那么相当于需要这么多次询问某个边界的某一段的左侧反向能到起始行的点的权值和。
但这可以看成关于行数,边界和向上能到的高度的三维数点,因此单次复杂度 ,总复杂度 ,显然过不去。
另一个想法是放弃维护边界直接求并,这样的好处是可以去掉最后一维(事实上上一个做法如果没有最后一维,可以直接二维前缀和做到 )。首先考虑单个询问,如何求一些分界点出发能到的点集的并。那么一个点能到当且仅当它被某个分界点向下的左右边界包含,且它能到分界点所在的行(这在一次分治中是固定的)。首先考虑两个分界点能到的区域的并,那么考虑减去相交部分。但可以发现一个问题是第一个的右边界和第二个的左边界间可能有多次相交,例如如下情况:
S###T..
...#...
.#..A#.
.#####.
.#.....
......B
此时两次相交中间可能有一些不被包含的点(例如 B
左上的部分)。但根据平面的性质可以发现,如果这些点能到达分界行,由于两个起点已经有路径完全包围了它,那么它向上的路径必定和某个起点的路径相交,因此一定有一个起点能到达它,矛盾。这说明这些实际上没有交的点一定会被“能反向向上到达分界行”的条件处理掉,那么可以不考虑这些点。
因此如果两个点能到达的区域在某一行相交了,那么可以认为它们接下来都是相交的。这样求两个的交只需要知道它们边界第一次相交的行,在下面合并即可。再考虑维护答案,相当于一些边界在某一行之后就不需要了,那么记 表示第 个分界点右边界前 行左侧所有满足条件的点的权值和,类似定义 ,预处理后即可直接求出答案。
此时如果每个分界点都能到最后一行,那可以发现多个求并只需要减去每一对相邻的交。但问题是每个分界点只能到前若干行,因此还需要进行分析。一种直接处理单次询问的方式是,顺序考虑每一行,同时维护合并相邻两部分和删去一个分界点,只需要支持求任意两个起点第一次相交的位置,再用类似求两个的并的方式算答案即可。
然后考虑如何处理多次询问。如果只考虑一个起始行的询问,那根据之前的分析这些询问能到的分界点是删去一些不能到起始行的分界点后的一个区间,且这个区间一定单调向右移动。因此维护这一行的询问需要支持两种操作:向两侧加入或删除一个分界点。
考虑如何在一侧加入一个分界点,不妨设加到左侧。根据之前从上往下的过程,可以发现如果它和后面的某一个合并了,那么这一部分集合没有影响:只需要将最左侧的左边界换成当前的就行。但可能接下来右边合并的那个被删掉了(它只能到当前行),然后它又变回还没有被合并的状态,这样继续向下直到左侧这个被删掉。
可以发现这个过程只和每一行当前第一个剩余的位置有关。因此考虑从左往右维护所有分界点向下深度构成的单调栈,那么相当于依次判断它与单调栈上每一段深度(行)对应的分界点的交,只取这一段内的边界贡献答案。注意到加入它之后操作的次数等于单调栈删去段数加上 ,所以复杂度均摊线性。
然后考虑删除,一种可能可行的方式是类似上面的过程倒着做,但还有更简单的做法:考虑维护每个分界点能到的部分中有多少被之前的覆盖了,然后加入时每次算一个区域内的交时就给后面那个点的这个值加上这次求出的交集,这样删的时候直接减即可。
这样均摊都是线性的,有一些实现细节。那么最后只需要求出之前的 以及支持求出任意两个分界点出发第一个能相遇的行。
只需要求出每个点能到的左右边界即可,然后考虑相遇的行。因为平面上的单调性,可以发现如果固定 ,随着 递增 相遇的行数(在只考虑相遇的 )时显然是单调不降的(否则考虑相交,左侧的 可以得到更早的相遇)。进一步分析容易得到如下方式:
枚举 ,对应每个 从左到右枚举 ,枚举时记录当前考虑的行数 。对于每个 ,增大 直到它们在第 行能相遇或者有一个到不了第 行,此时即可得到答案。
证明:如果三个都能到第 行,且前两个在第 行不相遇,那第 个在第 行也不可能相遇。
那么可以平方复杂度预处理相交情况,然后使用上述做法即可。设当前矩阵为 ,则复杂度为 。
最后考虑分治,因为复杂度有 ,所以不能直接分行。注意到沿对角线翻转后问题是不变的,那么每次分较长的一边即可。
复杂度 ,实际上常数不小,完全跑不过一些卡常
Code
代码可能有一点混沌(
#include<cstdio>
#include<vector>
using namespace std;
#define N 1591
int n;
char s[2][N][N];
int as[N][N];
int lb[N][N],rb[N][N],li[N][N],ri[N][N];
int vis[N][N],sl[N][N],sr[N][N];
int ip[N][N],tp[N][N],dep[N];
int vi[N],st[N];
void add(int fg,int x,int y,int v)
{
if(fg)x^=y^=x^=y;
as[x][y]+=v;
}
int que(int fg,int x,int y)
{
if(fg)x^=y^=x^=y;
return as[x][y];
}
#define S s[fg]
void solve(int lx,int rx,int ly,int ry,int fg)
{
if(lx==rx&&ly==ry)
{
if(S[lx][ly]!='#')add(fg,lx,ly,S[lx][ly]-'0');
return;
}
if(rx-lx<ry-ly)lx^=ly^=lx^=ly,rx^=ry^=rx^=ry,fg^=1;
int mid=(lx+rx)>>1;
solve(lx,mid,ly,ry,fg);solve(mid+1,rx,ly,ry,fg);
for(int i=ly;i<=ry;i++)lb[mid][i]=rb[mid][i]=i;
for(int i=ly;i<=ry;i++)if(S[mid][i]!='#'&&S[mid+1][i]!='#')vis[mid+1][i]=1;
for(int i=mid+1;i<=rx;i++)for(int j=ly;j<=ry;j++)if(S[i][j]!='#')vis[i][j]|=vis[i-1][j]|vis[i][j-1];
for(int p=mid+1;p<=rx;p++)
{
int nw=ry,mn=ry+1;
for(int i=ry;i>=ly;i--)
{
while(nw>=lb[p-1][i])
{
if(S[p-1][nw]!='#'&&S[p][nw]!='#'&&(vis[p-1][nw]||p==mid+1))mn=nw;
nw--;
}
lb[p][i]=mn;
}
nw=ly,mn=ly-1;
for(int i=ly;i<=ry;i++)
{
while(nw<=rb[p-1][i])
{
if(S[p-1][nw]!='#'&&S[p][nw]!='#'&&mn<nw&&(vis[p-1][nw]||p==mid+1))
{
mn=nw;
while(mn<ry&&S[p][mn+1]!='#')mn++;
}
nw++;
}
rb[p][i]=mn;
}
}
for(int i=ly;i<=ry;i++)
for(int j=mid+1;j<=rx;j++)
if(lb[j][i]>rb[j][i])
{
for(int k=j;k<=rx;k++)lb[k][i]=ry+1,rb[k][i]=ly-1;
break;
}
for(int i=ly;i<=ry;i++)
if(S[mid][i]!='#'&&S[mid+1][i]!='#')li[mid][i]=ri[mid][i]=i;
else li[mid][i]=ry+1,ri[mid][i]=ly-1;
for(int i=mid;i>=lx;i--)for(int j=ry;j>=ly;j--)
{
if(i<mid)li[i][j]=ry+1,ri[i][j]=ly-1;
if(S[i][j]!='#'&&S[i+1][j]!='#'&&i<mid)
{
if(li[i][j]>li[i+1][j])li[i][j]=li[i+1][j];
if(ri[i][j]<ri[i+1][j])ri[i][j]=ri[i+1][j];
}
if(S[i][j]!='#'&&S[i][j+1]!='#'&&j<ry)
{
if(li[i][j]>li[i][j+1])li[i][j]=li[i][j+1];
if(ri[i][j]<ri[i][j+1])ri[i][j]=ri[i][j+1];
}
}
for(int i=mid+1;i<=rx;i++)for(int j=ly;j<=ry;j++)vis[i][j]=vis[i][j-1]+(S[i][j]-'0')*vis[i][j];
for(int i=ly;i<=ry;i++)
{
sl[mid][i]=sr[mid][i]=0;;
for(int j=mid+1;j<=rx;j++)
{
sl[j][i]=sl[j-1][i];sr[j][i]=sr[j-1][i];
if(lb[j][i]<=rb[j][i])sr[j][i]+=vis[j][rb[j][i]],sl[j][i]+=vis[j][lb[j][i]-1];
}
}
for(int i=mid+1;i<=rx;i++)for(int j=ly;j<=ry;j++)vis[i][j]=0;
for(int i=ly;i<=ry;i++)
{
int nw=mid+1;
for(int j=i+1;j<=ry;j++)
{
ip[i][j]=nw;
while(rb[nw][i]<lb[nw][j])
{
if(nw==mid||lb[nw][j]>rb[nw][j]||lb[nw][i]>rb[nw][i]){ip[i][j]=-1;break;}
ip[i][j]=++nw;
}
}
}
for(int i=lx;i<=mid;i++)for(int j=ly;j<=ry;j++)if(S[i][j]!='#')
{
tp[i][j]=i;
if(i>lx&&S[i-1][j]!='#'&&tp[i-1][j]<tp[i][j])tp[i][j]=tp[i-1][j];
if(j>ly&&S[i][j-1]!='#'&&tp[i][j-1]<tp[i][j])tp[i][j]=tp[i][j-1];
}
for(int i=ly;i<=ry;i++)
{
dep[i]=mid;
for(int j=mid+1;j<=rx;j++)if(lb[j][i]<=rb[j][i])dep[i]=j;
}
for(int p=lx;p<=mid;p++)
{
int r1=0,sv=0,ls=ry+1,rs=ry;
for(int i=ry;i>=ly;i--)if(li[p][i]<=ri[p][i])
{
while(ls>li[p][i])
{
ls--;vi[ls]=0;
if(S[mid][ls]!='#'&&S[mid+1][ls]!='#'&&tp[mid][ls]<=p)
{
vi[ls]=que(fg,mid+1,ls);sv+=vi[ls];
int li=mid;
while(r1)
{
if(st[r1]>rs)break;
if(ip[ls][st[r1]]!=-1)
{
int l2=ip[ls][st[r1]],r2=dep[st[r1]];
if(l2<=li)l2=li+1;if(r2>dep[ls])r2=dep[ls];
int vl=sr[r2][ls]-sr[l2-1][ls]-sl[r2][st[r1]]+sl[l2-1][st[r1]];
if(l2>r2)vl=0;
vi[st[r1]]-=vl;sv-=vl;
li=dep[st[r1]];
}
if(dep[ls]>=dep[st[r1]])r1--;
else break;
}
st[++r1]=ls;
}
}
while(rs>ri[p][i])sv-=vi[rs],rs--;
add(fg,p,i,sv);
}
}
}
#undef S
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%s",s[0][i]+1);
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)s[1][i][j]=s[0][j][i];
solve(1,n,1,n,0);
long long su=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(s[0][i][j]!='#')
su+=(s[0][i][j]-'0')*(as[i][j]-s[0][i][j]+'0');
printf("%lld\n",su);
}
AGC027D Modulo Matrix
Problem
给定 ,你需要构造一个 的矩阵,使得所有数为不超过 的正整数且两两不同,且对于所有矩阵中相邻(四连通)的数 , 的值相同。
Sol
设取模的结果为 。如果方案中有一条很长的递增的路径,那么这条路径一定有很长的等差数列段,因为如果 ,那么一定有 ,但很长的等差数列会使得数列其它方向难以达到限制,因此很长的递增路径一定不行。
那么考虑一种极端的方式:要求方案中递增路径的长度不超过 ,即考虑将网格黑白染色,要求黑色格子的数大于相邻的白色格子。这样就变成黑色格子对白色格子取模等于 。从而一个黑色格子的值应当是相邻白色格子权值的 lcm 加上 。那么容易发现这种构造下应该让 ,问题变为找到一种往白色格子里面填不同数的方式,使得一个黑色格子相邻的格子权值 lcm 小于 。
最 naive 的方式是直接填最小的若干数再填乘积,但这样值域是 的。
注意到求的是四个数的 lcm,因此可以考虑填一些使得相邻的数 gcd 不为 的。例如,考虑取大于 的一些质数,每一行依次填 。这样每个位置只会遇到三个质数,同时如果每一列都顺序填每个数纵向只会遇到两个标号。显然黑色格子的值也是不同的(三行的质数乘上某个 )。同时黑色格子值至少是 ,因此两种颜色之间有问题。值域为 ,算出来上界大概 ,但把质数一大一小填进去后构造出来大概 ,正好可以通过。(实际上标号也可以卡掉一点,但这里不需要)
横向构造还是不够优秀:一个数和三个位置相邻。一种改进方式是依次填 的倍数,但这里限制填奇数倍,从而白色格子不会出现相同的数。那么每个数相邻的三个行只有两个 ,从而值域只有 。但这样有一些小问题:首先可以发现此时第 行相邻的三个数的 lcm 是相同的,那么这两行的黑色格子值也相同。一种方式是给奇数行 前再乘一个 ,这样显然解决了问题(每个位置的 lcm 本来正好有一个 的因子,那么 后判断 的次数一定可以将乘 的部分和不乘 的部分分开)。其次此时黑色格子的值已经只有 级别了,而白色格子最大可以达到 ,因此可能和白色格子的值相同。一种简单的实现方式是不从 开始,而从一个 级别的下标开始,这样就不重复了。值域 但常数不小,这里可以做到 。
考虑其它让一个数和两种颜色相邻的方式,容易发现斜向填数:对某个方向上的每条对角线填 的倍数,然后在另一个方向的对角线上依次乘 。这样显然每个位置只和两个 以及两个标号相同,那么直接就是 。同时直接的实现也不需要考虑之前的那些问题,因为 只出现在中间,但此时的质数已经有 大小了。常数相对更小,这里可以做到
Code
Sol 1:
#include<cstdio>
using namespace std;
#define N 505
#define ll long long
int vl[N*3],n,ct,d[4][2]={-1,0,1,0,0,1,0,-1};
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll as[N][N],mx;
int main()
{
for(int i=501;i<=10000;i++)
{
int fg=0;
for(int j=2;j<=100;j++)if(i%j==0)fg=1;
if(!fg)vl[++ct]=i;
}
int li=1,ri=500;
for(int i=1;i<=500;i++)
{
int nw,tp=1;
if(i&1)nw=vl[ri--];else nw=vl[li++];
for(int j=(i+1)%2+1;j<=500;j+=2)as[i][j]=tp*nw,tp++;
}
for(int i=1;i<=500;i++)
for(int j=1;j<=500;j++)
if((i+j)&1)
{
ll st=1;
for(int s=0;s<4;s++)
{
int vl=as[i+d[s][0]][j+d[s][1]];
if(vl)st=st/gcd(st,vl)*vl;
}
as[i][j]=st+1;
if(st>mx)mx=st;
}
// printf("%lld\n",mx);
scanf("%d",&n);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=n;j++)
printf("%lld ",as[i][j]);
}
Sol 2:
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 505
#define ll long long
int vl[N*3],n,ct,d[4][2]={-1,0,1,0,0,1,0,-1};
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll as[N][N],mx,tp[N*N];
int main()
{
for(int i=521;i<=10000;i++)
{
int fg=0;
for(int j=2;j<=100;j++)if(i%j==0)fg=1;
if(!fg)vl[++ct]=i;
}
for(int i=1;i<=500;i++)
{
int nw,tp=13;
if(i&1)nw=vl[i/2+1];else nw=vl[i/2+1]*2;
for(int j=(i+1)%2+1;j<=500;j+=2)as[i][j]=tp*nw,tp+=2;
}
for(int i=1;i<=500;i++)
for(int j=1;j<=500;j++)
if((i+j)&1)
{
ll st=1;
for(int s=0;s<4;s++)
{
int vl=as[i+d[s][0]][j+d[s][1]];
if(vl)st=st/gcd(st,vl)*vl;
}
as[i][j]=st*(1+i%2)+1;
if(as[i][j]>mx)mx=as[i][j];
}
// printf("%lld\n",mx);
scanf("%d",&n);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=n;j++)
printf("%lld ",as[i][j]);
}
Sol 3:
#include<cstdio>
using namespace std;
#define N 505
int vl[N*3],n,ct,d[4][2]={-1,0,1,0,0,1,0,-1};
long long gcd(long long a,long long b){return b?gcd(b,a%b):a;}
long long as[N][N],mx;
int main()
{
for(int i=501;i<=10000;i++)
{
int fg=0;
for(int j=2;j<=100;j++)if(i%j==0)fg=1;
if(!fg)vl[++ct]=i;
}
for(int i=1;i<=500;i++)as[i][i]=vl[1]*i;
int st=2;
for(int i=2;i<=500;i+=2){for(int j=1;i+j<=500;j++)as[i+j][j]=vl[st]*(j+i/2),as[j][i+j]=vl[st+1]*(j+i/2);st+=2;}
for(int i=1;i<=500;i++)
for(int j=1;j<=500;j++)
if((i+j)&1)
{
long long st=1;
for(int s=0;s<4;s++)
{
int vl=as[i+d[s][0]][j+d[s][1]];
if(vl)st=st/gcd(st,vl)*vl;
}
as[i][j]=st+1;
if(as[i][j]>mx)mx=as[i][j];
}
//printf("%lld\n",mx);
scanf("%d",&n);
for(int i=1;i<=n;i++,printf("\n"))
for(int j=1;j<=n;j++)
printf("%lld ",as[i][j]);
}
AGC027E ABBreviate
Problem
给一个长度为 的包含 的字符串,你可以进行如下操作:
- 将一个子串
aa
变成b
- 将一个子串
bb
变成a
求有多少个字符串可能被达到。答案模
Sol
从最后的串开始倒着考虑,每个字符一定对应原来的一段,因此考虑如下问题:一段需要满足什么条件才能变为一个字符,同时它能变为哪种单个字符。
考虑手玩(例如发现 aaaa
-> a
),或者考虑找不变量。可以发现如下结果:在 下考虑,将 a
看成 ,b
看成 ,那么操作相当于选两个数加起来,但不能出现 。那么可以发现合并不影响所有字符 下的和。因此可能变成的字符是唯一的:计算 的和即可。
再考虑什么情况下能变为单个字符。考虑经典的 atc 做法:首先如果第一步就不能操作(即字符串为 ab
交错),那显然不行。然后可以发现:
如果 , 的总和模 不是 且 中存在两个相邻的相同字符,那么 可以变为单个字符。
证明:对 归纳。如果 中存在至少两个极长连续相同字符段,那么找到长度至少是 的一段,它至少和一个另一种字符相邻,那么在边界上操作即可得到新的长度为 的段(...aab
-> ...bb
)。否则,考虑直接操作一端,唯一有问题的情况是 ( 直接变成单字符, 之前一段就还存在),但此时总和模 是 ,因此不存在问题。
那么判断一个目标串 是否合法可以看成如下问题:是否可以将 分成 段,使得第 段满足上述条件且模 与 的每一个字符相同。显然直接 dp 无法接受。尝试贪心(直接猜就过了),但问题是它不满足包含单调:子串合法不一定整体合法,例如 a
和 aba
。所以看上去不能直接做。
不能为 abab...
的限制使得不能直接贪心,考虑能否去掉这个限制。可以发现这确实是可行的:
考虑去掉限制后的合法方案,此时唯一在之前不合法的情况是出现了 abab...
的段。但对于这样的段,可以将两侧的一个 ab
移到两侧的段内部。那么考虑通过移动调整。如果 存在两个相邻相同字符,那可以考虑将附近不合法的段中多出来的 ab
都扔过来。如果这两个字符在划分中在同一段那直接可以,否则情况一定类似 aba|a
(如果两侧都不存在多余的,那也不需要调整),此时可以将 ba
移到右侧以得到一段内存在相邻相同字符。那么只要 存在两个相邻相同字符,两种情况就一定等价。而如果 本身就无法操作,那答案只能是 。
因此特判 本身无法操作的情况,其余情况可以扔掉子段的限制。此时只剩下和的限制,这显然可以贪心。另一种解释是考虑模 的前缀和,可以发现划分变为在前缀和序列中选择一个子序列(必须选择结尾),使其匹配 的前缀和序列。去掉结尾就变成了完全的子序列问题。
那么问题可以看成,求有多少个 组成的序列 满足 与 前缀和的最后一位相等,去掉最后一位后是子序列,且不存在相邻两位相同(前缀和的限制)。判定可以贪心,因此可以对贪心过程进行计数:记 表示当前匹配到第 个位置的方案数,中间的转移枚举下一个填啥然后转移过去,这类似于一个子序列自动机。对于结尾,枚举上一个字符贪心划分到哪即可。
复杂度
可以发现这题和两年后的 arc110e(自我批评:那个题的题解没有证最后一部分)是完全一样的,唯一的区别在于 和 ,但是 agc 3600 -> arc 2900
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 100500
#define mod 1000000007
int n,nt[N],su[N],nt2[N][3],as,dp[N];
char s[N];
int main()
{
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++){su[i]=(su[i-1]+s[i]-'a'+1)%3;}
nt[n+1]=n+1;nt2[n+1][0]=nt2[n+1][1]=nt2[n+1][2]=n+1;
for(int i=n;i>=0;i--)
{
nt[i]=nt[i+1];nt2[i][0]=nt2[i+1][0];nt2[i][1]=nt2[i+1][1];nt2[i][2]=nt2[i+1][2];
if(s[i]==s[i+1])nt[i]=i+1;
nt2[i][su[i+1]]=i+1;
}
dp[0]=1;
for(int i=0;i<=n;i++)
{
int tp=nt[i]-1;
if(s[i+1]=='a')dp[nt2[tp][(2+su[i])%3]]=(dp[nt2[tp][(2+su[i])%3]]+dp[i])%mod;else dp[i+1]=(dp[i+1]+dp[i])%mod;
if(s[i+1]=='b')dp[nt2[tp][(1+su[i])%3]]=(dp[nt2[tp][(1+su[i])%3]]+dp[i])%mod;else dp[i+1]=(dp[i+1]+dp[i])%mod;
if(!(su[n]-su[i])%3&&i)as=(as+dp[i])%mod;
}
if(nt[0]==n+1)as=1;
printf("%d\n",as);
}
AGC027F Grafting
Problem
给两棵 个点的树 ,它们有相同的点集。
你可以对 进行如下操作:
选择一个叶子,删去它和父亲的连边,然后将它任意连到一个点上。
但有一个额外限制:每个点最多被操作一次(原题是操作后染色)
求出将 变为 需要的最少操作次数,或输出无解。
多组数据,
Sol
考虑操作的形式。设当前操作点为 ,操作后连向了 。这之后 不能再操作,如果之后也不操作 ,那么这次操作必须将 接到它在 上相邻的某个点。但也有可能它现在连向的 之后再操作断掉这条边,这种情况是难以处理的。
考虑会有多少这种情况出现。注意到如果当前树上有两个被操作过的点,那它们间路径上的点就不能再被操作了,因为它们不可能再成为叶子。因此考虑第二次操作,如果第二次操作连向 ,则它是第一种情况,而如果不连向 ,则第二次操作的点到第一次操作的点必定经过第二次连向的点,那么这次操作一定属于第一种情况。可以发现对于之后的操作都成立,那么有如下结论:
除去第一次操作外,所有操作均满足加入的边在之后不被断掉。
首先考虑所有操作都满足这一限制的情况。因为每条边只能加入一次,操作次数一定不超过 。考虑第一次操作连向的点,这个点一定不会操作,因此存在一个不被操作的点。(实际上看操作次数也可以看出来)。考虑枚举一个不被操作的点,那么这个点是固定的。考虑以其为根,这样变成了一个有根树上的问题。在根固定有根树下每个点有唯一的父亲节点,可以发现操作相当于改一个点的父亲。那么容易发现如下事实:
如果一个点在 中的父亲相等,则它一定不会进行操作,否则一定进行一次操作。
这是显然成立的。那么枚举根后操作次数也可以唯一确定。同时可以去掉一些不合法情况:如果两个不操作的点中间有一个需要操作的点,则无解。
然后考虑剩余每个点操作的顺序。显然有如下两组限制:
- 如果在 中 是 的父亲,则 比 先操作。
- 如果在 中 是 的父亲,则 比 先操作。
证明:必要性显然。对于充分性,考虑一组满足上述条件的方案,则可以发现操作一个点时它的子树内其它点都被删掉了,同时它连向的点也已经移过去了,那么可以进行操作。
限制是顺序关系,那么只需要连出所有边,拓扑排序判是否存在环即可。枚举起始点后复杂度 。
然后考虑第一次操作不满足的情况。此时第一次操作后以 为根,它只有一个儿子 ,且 还会操作,那么 子树内也都要操作,从而步数一定是 ,那么可以先判定上一种情况再做这一种。
注意到第一次操作后就变成了之前的情况,因此考虑 枚举第一次操作,然后对当前点用之前的方式判定即可。
复杂度
也有更统一的实现方式:直接枚举第一次操作,然后以操作点为固定点做之前的部分,同时记录步数。但这样需要额外判答案是 的情况。
Code
看不下去当年写的tarjan判环,重写了
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
#define N 55
int T,n,a,b,s[N][2],t[N][2],d1[N],ds1[N][N],ds2[N][N],f1[N],f2[N];
int d[N];
vector<int> nt[N];
queue<int> si;
void adde(int f,int t)
{
nt[f].push_back(t);d[t]++;
}
bool check()
{
int ct=0;
for(int i=1;i<=n;i++)if(d[i]==0)si.push(i);
while(!si.empty())
{
int u=si.front();
si.pop();ct++;
for(int i=0;i<nt[u].size();i++)
{
d[nt[u][i]]--;
if(!d[nt[u][i]])si.push(nt[u][i]);
}
}
for(int i=1;i<=n;i++)d[i]=0,nt[i].clear();
return ct==n;
}
int solve()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)d1[i]=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)ds1[i][j]=ds2[i][j]=i==j?0:1e8;
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),s[i][0]=a,s[i][1]=b,ds1[a][b]=ds1[b][a]=1,d1[a]++,d1[b]++;
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),t[i][0]=a,t[i][1]=b,ds2[a][b]=ds2[b][a]=1;
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)
ds1[i][j]=min(ds1[i][j],ds1[i][k]+ds1[k][j]),ds2[i][j]=min(ds2[i][j],ds2[i][k]+ds2[k][j]);
int fg1=1,as=1e8;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(ds1[i][j]!=ds2[i][j])fg1=0;
if(fg1)return 0;
for(int i=1;i<=n;i++)if(d1[i]==1)
for(int j=1;j<=n;j++)if(j!=i)
{
for(int k=1;k<n;k++)if(ds1[j][s[k][0]]<ds1[j][s[k][1]])f1[s[k][1]]=s[k][0];else f1[s[k][0]]=s[k][1];
for(int k=1;k<n;k++)if(ds2[i][t[k][0]]<ds2[i][t[k][1]])f2[t[k][1]]=t[k][0];else f2[t[k][0]]=t[k][1];
f1[j]=i;
int ct=1,fg=1;
for(int k=1;k<=n;k++)if(k!=i)
if(f1[k]!=f2[k])
{
if(f1[k]!=i&&f1[f1[k]]!=f2[f1[k]])adde(k,f1[k]);
adde(f2[k],k);ct++;
}
else if(f1[k]!=i&&f1[f1[k]]!=f2[f2[k]])fg=0;
if(check()&&as>ct&&fg)as=ct;
}
return as<=n?as:-1;
}
int main()
{
scanf("%d",&T);
while(T--)printf("%d\n",solve());
}
AGC026D Histogram Coloring
Problem
有一个 行 列的矩形网格,在第 行只保留下面 个格子。对剩余部分每个格子染 01
两种颜色之一,满足如下限制:
对于任何一个完整存在的 的子矩形,其中正好有两个 0
。
求方案数,答案模
Sol
考虑方案满足的性质,一种分析方式是从考虑从一列到下一列。首先考虑两列高度相等的情况,此时考虑第一列每两个相邻格子可以发现:
如果两个相邻格子是 01
或 10
,则下一列这两个格子也可以是 01
或 10
,即可以反转也可以不反转,而如果相邻两个格子相同,则下一列这两个格子必须反转。那么可以发现如下结论:
下一行一定是当前行或者当前行反转 01
后的结果,后者一定可行,前者可行当且仅当上一行不存在两个相邻位置相同。
现在考虑每一列高度不同的情况。可以发现反转不改变相邻位置相同的情况,唯一的变化是可能随着高度降低,之前高处相邻相同的部分被删掉了。因此考虑状态记为最小的 使得这一列第 个相同,考虑转移:
- 如果当前行高度 小于等于 ,那么上一列完全覆盖了下面的部分。此时如果 或者 不存在,那么第 位不存在相邻相同,因此这部分乘 转移到不存在 的状态,剩余状态可以发现一定转移到对应的 。
- 如果 ,那么高位都可以任意填(显然高位不存在合法条件)。如果已经有 了,那么高位不会再影响状态,只需要乘 即可。如果还不存在 ,则考虑高位第一个相同出现的位置,可以发现转移相当于将 中间这一段变为一个 的等比数列。
那么转移相当于整体乘,后缀求和然后合并起来,往后面加一段等比数列(不存在 的单独维护)。暴力按照 分段维护是 的,但如果用栈维护每一个等比的段然后整体乘打标记则容易做到 。
也可以考虑换一个顺序。之前是按照列做,现在考虑按照行从高往低做。这相当于在高度的笛卡尔树上自下向上合并。可以发现相当于维护若干个子网格,支持 次如下操作:
- 向下面加若干行。
- 将两个网格中间加一个格子拼在一起。
这里甚至只有合并,因此只需要记录当前最下面一行是否存在相邻相同,为了合并再记录当前最下面一行两侧的值是多少。此时第一种操作根据是否存在相邻相同,向下 行分别有 种方式,但还需要考虑两侧取值,因为操作只有反转和直接复制,那么最后有一半反转一半不反转,容易处理(如果是 的情况则判断奇偶性)。合并直接枚举即可。复杂度也是
Code
原来的代码看不下去,重写了第一类的 。第二类咕了
#include<cstdio>
using namespace std;
#define N 105
#define mod 1000000007
int n,v[N],st[N],vl[N],ct,su,as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
v[0]=v[n+1]=1;su=(mod+1)/2;as=1;
for(int i=1;i<=n+1;i++)
if(v[i]>v[i-1])
{
as=1ll*as*pw(2,v[i]-v[i-1])%mod;
su=2ll*su*pw((mod+1)/2,v[i]-v[i-1])%mod;
st[++ct]=v[i]-1,vl[ct]=su;
}
else
{
while(st[ct]>v[i]-1)
if(st[ct-1]>=v[i]-1)su=(su+1ll*vl[ct]*(pw(2,st[ct]-st[ct-1])-1))%mod,ct--;
else su=(su+1ll*vl[ct]*(pw(2,st[ct]-v[i]+1)-1))%mod,vl[ct]=1ll*vl[ct]*pw(2,st[ct]-v[i]+1)%mod,st[ct]=v[i]-1;
su=2*su%mod;
}
printf("%d\n",1ll*su*as%mod);
}
AGC026E Synchronized Subsequence
Problem
给一个长度为 的包含 ab
的字符串 ,保证两种字符出现次数相等。
你需要选择 的一个子序列,满足如下限制:
从左到右第 个 a
被选当且仅当从左到右第 个 b
被选。
求你能选出的字典序最大的字符串。
Sol
首先考虑最大化开头,即最大化开头 b
的长度。这相当于选出尽量多的 b
满足这些 b
对应的 a
都在每一个 b
之后。考虑能否找到 个,然后可以发现:
能找到至少 个当且仅当存在 的一个前缀,其中 b
的个数减去 a
的个数大于等于 。
证明是显然的,只需要从选择方案的分界点划分开即可。
因此考虑看成类似括号序列:将 a
看成 ,b
看成 ,那么开头 数量的最大值即为前缀和序列的最大值。
考虑选了一组这样的 b
之后接下来如何最大化字典序。考虑此时在后面 个 a
中间的 b
,根据顺序它们对应的 a
都在之前所有 a
的后面。那么为了最大化第一个 a
后面 b
的数量,在前两个 a
中间出现的 b
一定都要选,类似可以发现前 个 a
之间出现的 b
都一定选。而此时新加入的 a
之间还可以重复该操作。那么最后可以得到如下性质:
在选择了一个前缀和为 的分界点并在左侧选 个之后,最优解一定将分界点到接下来第一个前缀和为 的点这一段全部选择。
那么考虑按照前缀和为 的点将 分段。分段后同时可以发现每一段内部一定所有 b
在对应 a
前出现或者反过来。由于在前缀和为 的地方分段,可以发现第 个 a
和第 个 b
一定在同一段内,进而问题可以变为在每一段内选择一个字符串(可以为空),最大化拼接后的字典序。
这里先只考虑第一种情况(感性理解,选一段 a
开头的不如把它删掉,除非在结尾)。可以发现最优解一定是选择一段,在这一段内部选择一个前缀和最大的位置进行上述操作,然后在接下来的段中继续选择。考虑从每一段的角度进行分析。
首先遇到的一个问题是每一段可能有多个前缀和最大的点,从而有多种可行选择,这对于拼接难以处理。但注意到拼接时字典序与接下来的串有关的情况只会在有一个选择是另一个前缀的情况下出现(考虑第一个串),考虑此时是否有这种情况。从前缀和的角度考虑,每种方案是从 增长到 ,然后选择一个从 到 的后缀。但操作只在这一段内部,因此前缀和不可能在中间就变成 ,即每种方案前缀和只有开头结尾是 。因此可以发现不存在一个方案是另一个的前缀(所有方案长度不同),因此每一段可以确定最优的方式:选择所有合法结果中字典序最大的即可。
然后考虑不同段之间的问题,相当于有若干个字符串排成一列,你需要保留一些,最大化拼接出来的字典序。没有额外限制还是很难,但这里也可以类似地发现除了完全相等的情况外,不会有一个串是另一个的前缀。如果也没有完全相等的,那类似之前的方式可以证明最优解是每次在当前后缀中选择字典序最大的一个串,然后从那里开始向后,证明是显然的。如果有重复,可以发现选先出现的一定不差。那么可以得到这部分的方案:选择字典序最大的串(有多个选最靠前的),然后在后缀中继续考虑。
最后来考虑之前被忽略的 a
在 b
前面的部分。根据之前的分段,这部分相当于在之前的段间加入了一些 a
在 b
前面的段。但可以发现这样的段里面选一些字符得到的结果一定是 a
开头的,那么如果后面有 b
开头的一定不优。因此最后一个 b
在 a
前面的段之前的这部分都可以删掉,只保留最后的部分,那么变为下面的问题:
当前每一对都满足 a
在 b
前面,求最大字典序。
可以发现此时只能 a
开头,但第二个字符就可以是 b
(实在不行就选一对),然后下一个字符如果存在只可能是 a
,以此类推。可以发现答案一定是 abab...
,问题变为最多能选出多少对。那显然方案是每次尝试选最靠前的能选的一对,向后继续。那么这部分就做完了。
考虑实现。即使暴力选择后缀,下一步暴力选择字典序最大的字符串,复杂度也是 的,可以通过。
考虑后缀最大字符串的问题,考虑倒着扫,通过 后缀的最大值和第 个串比较求出 后缀的答案。可以发现如果比较复杂度是 ,那么总复杂度不超过所有串串长的和,即 。
然后考虑第一步,相当于在若干个后缀(前面的若干个 b
可以不考虑)中选出字典序最大的。那后缀排序一下就可以做到更优的复杂度。
复杂度 ,也可以通过上述实现做到 甚至
Code
懒得写后缀排序了(也不会写线性)
#include<cstdio>
#include<string>
using namespace std;
#define N 6050
int n,su[N],ri,ct,nt[N];
char s[N];
string si[N];
int main()
{
scanf("%d%s",&n,s+1);n*=2;
for(int i=1;i<=n;i++)su[i]=su[i-1]+(s[i]=='b'?1:-1);
for(int i=1;i<=n;i++)if(su[i]>0)
{
int lb=i,rb=i;while(su[rb]>0)rb++;
ri=rb;
int mx=0;
for(int j=lb;j<=rb;j++)if(su[j]>mx)mx=su[j];
string as;
for(int j=lb;j<=rb;j++)if(su[j]==mx)
{
string tp;
for(int k=1;k<=mx;k++)tp+='b';
for(int k=j+1;k<=rb;k++)tp+=s[k];
if(tp>as)as=tp;
}
si[++ct]=as;i=rb;
}
int s1=0,c1=0;
for(int i=ri+1;i<=n;i++)if(s[i]=='b')
if(!s1)c1++,s1=-su[i];else s1--;
for(int i=ct-1;i>=0;i--)nt[i]=si[i+1]>=si[nt[i+1]]?i+1:nt[i+1];
for(int i=nt[0];i;i=nt[i])printf("%s",si[i].c_str());
for(int i=1;i<=c1;i++)printf("ab");
}
AGC026F Manju Game
Problem
有 个盒子排成一列,第 个盒子中有 枚硬币。两人轮流进行如下操作直到所有盒子被打开:
选择一个还没有被选择过的盒子,获得它里面的硬币。如果存在与上一次选择盒子相邻且没有被选中过的盒子,则这次只能在满足这一条件的盒子中选择,否则可以任意选择。
双方都想最大化自己得到的硬币数量,求双方最优操作下先手获得的硬币数量。
Sol
首先考虑进一步描述这个博弈的过程。先手第一次操作选择一个位置,后手接下来的一次操作可以看成选择一个方向,然后可以发现两人只能在这个方向上轮流选,直到这个方向选完。这时又回到了另一边没有限制的情况,然后是一个子问题。因此整个过程相当于先手选一个点,后手选一个方向,然后在这个方向上轮流拿完,接下来在另一边做子问题(先后手可能改变)。那么有naive的n^3做法
每次变为子问题时先后手可能改变,因此考虑双方策略时首先需要考虑先后手谁有优势。通过观察样例可以看出先手应该有一些优势,但这看起来不那么显然。
先考虑简单的策略。如果先手选择一个端点,那么游戏就直接结束了。选择左端点时,先手可以得到所有奇数位置的值,后手可以得到偶数位置的值。再考虑选右端点,那么如果 是偶数,先手就得到了另一部分的值。从而先手至少可以得到 的分数,而这至少是总数的一半。考虑 是奇数的情况,此时反过来没有效果,但仍然尝试得到类似的结论:考虑让先手每次任意选择一个偶数位置,那么无论后手怎么操作,转到另一边时先手仍然先操作,然后可以发现先手可以拿到所有偶数位置。因此有如下结论:
先手至少可以得到 的分数,从而先手不会劣于后手。
那么可以想象,第二步后手决定方向时,选能让自己成为先手的方向应该更优。那么考虑 是偶数的情况,此时如果先手选择中间的位置,后手的选择可以决定接下来谁先手,那么考虑后手选让自己成为先手的那一侧,这样后手在这一侧可以得到所有奇数(或偶数)位置的值,然后另一侧至少可以得到所有奇数位置和与偶数位置和的最大值。那么这样先手只能得到一侧所有偶数(或奇数)位置的值,以及另一侧两种值和的最小值,但这显然不大于 。那么有如下结论:
如果 为偶数,先手最优操作为选择端点,可以得到 的分数。
然后考虑奇数的情况。此时如果先手选择中间的奇数位置,那么后手可以类似之前的过程得到先手然后更优。因此先手只有两种可能的操作:
- 选择一个端点,获得所有奇数位置和的分数。
- 选择中间的偶数位置。那么此时后手会选择一个方向,然后变为另一个方向的问题。
那么整个操作过程相当于先手可以选一个偶数位置划分为两个区间,然后后手选择一个区间继续,先手也可以直接选择当前区间结束。可以发现设结束的区间为 ,则先手的分数为区间内的奇数和加上区间外的偶数和。
简化一下最后的权值,考虑定义一个区间的权值为区间内奇数位置减去偶数位置的和,则先手相当于需要最大化最后的区间的权值。那么考虑这样一个问题:
有一个长度为奇数的序列,每个奇数位置开头结尾的区间有一个权值。先手每次可以选择结束或者从一个偶数位置划分开,然后后手选择一侧继续。先手希望最大化结束时区间的权值,后手希望最小化。求最优策略下的结果。那么又有naive的n^3做法
这个博弈中权值只在最后得到,不会出现多部分相加。考虑二分,判断答案是否大于等于 。此时一个区间合法当且仅当它的权值大于等于 ,或者它能被一个偶数位置划分为两个合法的区间。考虑一直划分下去,可以发现如下结论:
答案大于等于 当且仅当原序列可以被若干个偶数位置划分为若干区间,每个区间权值都大于等于 。
证明:对于充分性,可以考虑将合法的划分一直划分下去,这样就能得到一个合法方案。对于必要性,考虑每次选第一个点划分,对后缀归纳即可。
那么二分答案后只需要判定后面这个问题。考虑直接 dp。记序列 的前缀和为 ,划分点 能转移到 当且仅当 ,那么扫过去记录当前可行的结尾划分点的最小 即可做到单次线性。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 300060
int n,v[N];
long long su[N],su1,su2;
bool check(long long l)
{
long long mn=0;
for(int i=1;i<=n;i+=2)
if(su[i]-mn>=l)
{
if(mn>su[i+1])mn=su[i+1];
if(i==n)return 1;
}
return 0;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
if(~n&1)
{
long long s1=0,s2=0;
for(int i=1;i<=n;i+=2)s1+=v[i],s2+=v[i+1];
printf("%lld %lld\n",max(s1,s2),min(s1,s2));
}
else
{
for(int i=1;i<=n;i++)su1+=v[i]*(i&1),su[i]=su[i-1]+v[i]*(i&1?1:-1),su2+=v[i]*(~i&1);
long long lb=0,rb=2e10,as=0;
while(lb<=rb)
{
long long mid=(lb+rb)>>1;
if(check(mid))as=mid,lb=mid+1;
else rb=mid-1;
}
printf("%lld %lld\n",su2+as,su1-as);
}
}
AGC025D Choosing Points
Problem
给定 ,考虑所有满足 的整点,共有 个点。给定
在这些点中选出 个点,满足如下限制:
选出的任意两点间欧几里得距离不等于 ,也不等于 。
构造任意方案,可以证明有解。
Sol
有两组限制,要求选出 的整点。因此一个naive的想法是一组限制做到 ,这样直接做两次即可。
但一组的情况也不好做,考虑找一些性质。
从 考虑,众所周知 只可能是 。那么可以发现 等于没有限制,而如果 ,则整点上 时一定有 。因此此时如果两个点间距离等于 ,那么它们的 坐标奇偶性一定不同。因此选出所有 坐标为偶数的点一定满足该限制,选出所有奇数的也可以,那么选较大的一侧即可做到 。事实上这相当于如下结论:
如果将所有距离为 的点对连边,则图为二分图,这样选出较大的一侧即可。
事实上这对所有情况都成立。考虑 的情况,此时可以发现如果 ,那么 ,因此按照 的奇偶性分类即可。最后是 ,此时 ,那么考虑把点按照 的奇偶性分为四部分,但此时只会在每一部分内部连边。考虑一部分内的情况,此时可以将 全部除以 , 除以 ,变为更小的情况。因此归纳即可证明原结论。
这样一组限制就可以做到保留一半以上,做两次即可。
考虑如何建图。暴力的方式是枚举点,再枚举 的差值,这样即可做到 。也可以预处理 可能的取值,实际上这并不多。
Code
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
#define N 100050
#define M 605
int n,d1,d2,head[N],cnt,col[M][M],col2[M][M],vis[M][M],ct,s1[N][2];
queue<int> tp;
void bfs(int x,int y)
{
vis[x][y]=1;
tp.push(x*1000+y);
while(!tp.empty())
{
int s=tp.front()/1000,t=tp.front()%1000;tp.pop();
for(int i=0;i<=ct;i++)
for(int j=s-s1[i][0];j<=s+s1[i][0];j+=s1[i][0]*2)
{
for(int k=t-s1[i][1];k<=t+s1[i][1];k+=s1[i][1]*2)
{
if(j>=1&&j<=n&&k>=1&&k<=n&&!vis[j][k])vis[j][k]=1,col[j][k]=col[s][t]^1,tp.push(j*1000+k);
if(!s1[i][1])break;
}
if(!s1[i][0])break;
}
}
}
int main()
{
scanf("%d%d%d",&n,&d1,&d2);n*=2;
ct=0;for(int i=0;i<=n;i++)if(d1>=i*i){int tp=sqrt(d1-i*i);if(i*i+tp*tp==d1)s1[++ct][0]=i,s1[ct][1]=tp;}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(!vis[i][j])bfs(i,j);
int tp=0;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)tp+=col[i][j]?-1:1;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)col2[i][j]=col[i][j]^(tp>=0),vis[i][j]=0,col[i][j]=0;
ct=0;for(int i=0;i<=n;i++)if(d2>=i*i){int tp=sqrt(d2-i*i);if(i*i+tp*tp==d2)s1[++ct][0]=i,s1[ct][1]=tp;}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(!vis[i][j])bfs(i,j);
tp=0;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)tp+=col[i][j]?-1:1;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)col[i][j]=col[i][j]^(tp>=0);
int ct=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(ct<n*n/4&&col[i][j]==1&&col2[i][j]==1)printf("%d %d\n",i-1,j-1),ct++;
}
AGC025E Walking on a Tree
Problem
给定一棵 个点的树以及 对 。
你可以对每一个给定的点对选择一个方向,然后你会在树上从选定的起点走到另一个点。
使用如下方式定义分数:
考虑每条边的每个方向,如果有一次行走以这个方向经过了这条边,则分数 。
求分数的最大值。
Sol
记不考虑方向时第 条边被经过的次数为 ,则显然答案上界是 。尝试构造达到这一上界。
自下向上考虑。首先所有 的边可以直接被删掉。对于一条 的边,可以发现经过它的路径的定向不改变这条边的贡献。那么考虑将这条边缩起来做定向,这显然不影响还原后这条边的贡献。(实际上 的边也可以缩)
这样之后剩余每条边的 都至少是 ,考虑能否达到 的上界。
考虑一个叶子 ,极限情况下只有两条路径经过 ,设为 。那么为了达到上界,这两条路径一定是一条到 ,一条从 出发。这样无论具体定向是啥,这两条路径的公共部分都达到了要求。考虑剩余部分。可以发现去掉公共部分后 剩余的是一条 到 的路径,反过来也一样。因此可以发现,这两条路径的定向可以看成一条 路径的定向。
此时考虑将两条路径合并为一条,然后将公共部分全部缩起来,这一步不影响缩起来边的贡献。可以发现其它边的 都不变,那么变成了更小的问题。而如果叶子出发的路径有至少三条,则选两条出来做即可。因此归纳可得上界一定能被达到。
那么使用之前的思路构造上界即可。直接缩边即可做到 。但也可以换一种方式实现:从下到上每次缩一个叶子,处理当前这个叶子连出的所有路径。可以发现一次不直接缩完也不影响正确性。如果对于每个叶子记录相关路径,则复杂度可以做到 ,但我懒了
Code
不想重写了
#include<cstdio>
using namespace std;
#define N 4050
int n,m,st[N],fg,head[N],cnt,vl[N],ct,s,t,as,v[N][2],ct2,is[N],nt[N],v2[N][2];
struct edge{int t,next,id;}ed[N*2];
struct sth{int f,t,v;};bool operator <(sth a,sth b){return a.v<b.v;}
void adde(int f,int t,int id){ed[++cnt]=(edge){t,head[f],id};head[f]=cnt;ed[++cnt]=(edge){f,head[t],id};head[t]=cnt;}
void adde2(int f,int t,int id){ed[++cnt]=(edge){t,head[f],id*2};head[f]=cnt;ed[++cnt]=(edge){f,head[t],id*2+1};head[t]=cnt;}
void dfs(int u,int fa)
{
if(!fg)st[++ct]=u;
if(u==t)fg=1;
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs(ed[i].t,u);
if(!fg)ct--;
}
void dfs2(int u,int fa)
{
for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs2(ed[i].t,u);
int ls=0;
for(int i=1;i<=ct2;i++)
if(v[i][0]==u||v[i][1]==u)
{
if(!ls)ls=i;
else
{
int l=v[ls][0],r=v[i][1];
if(v[ls][0]==u)l=v[ls][1],is[ls]=1;
if(v[i][1]==u)r=v[i][0],is[i]=1;
nt[ls]=nt[i]=++ct2;
v[ct2][0]=l;v[ct2][1]=r;
v[ls][0]=v[ls][1]=v[i][0]=v[i][1]=0;ls=0;
if(v[ct2][0]==v[ct2][1])v[ct2][0]=v[ct2][1]=0;
}
}
if(ls)
{
if(v[ls][0]==u)v[ls][0]=fa;
else v[ls][1]=fa;
if(v[ls][0]==v[ls][1])v[ls][0]=v[ls][1]=0;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)scanf("%d%d",&s,&t),adde(s,t,i);
for(int r=1;r<=m;r++)
{
scanf("%d%d",&s,&t);ct=fg=0;dfs(s,0);v[r][0]=s,v[r][1]=t;v2[r][0]=s;v2[r][1]=t;
for(int i=1;i<ct;i++)for(int j=head[st[i]];j;j=ed[j].next)if(ed[j].t==st[i+1]&&vl[ed[j].id]<2)vl[ed[j].id]++,as++;
}
printf("%d\n",as);
ct2=m;dfs2(1,0);
for(int i=ct2;i>=1;i--)
is[i]^=is[nt[i]];
for(int i=1;i<=m;i++)if(is[i])printf("%d %d\n",v2[i][1],v2[i][0]);
else printf("%d %d\n",v2[i][0],v2[i][1]);
}
AGC025F Addition and Andition
Problem
以二进制表示的方式给出两个数 ,其位数为 。
进行 次如下操作:令 等于 二进制按位与的结果,然后给 同时加上 。
求出 次操作后的 的二进制表示。
Sol
温馨提示:原题数据不强,一些能过原题的代码全 都过不去(WA或者TLE)。
(不知道对不对的)Sol 1:
注意到按位与的结果只和当前位有关,再加回去也只和高位有关。那么考虑记录当前位在所有 个时刻中所有的向前进位(及其时刻),这样从当前位可以直接推到下一位。最直接的实现是模拟当前位上的进位情况,但如果当前位出现了两个 ,那么将其转化为下一个时刻的向前进位。同时这里需要注意处理超过 时刻的情况。
直接做显然过不去。可以发现直接的问题是开头一段进位 (这里进位 表示两个数分别进位 ) 在前面没有数的情况下消不掉,然后就 了。但可以发现拿个 deque 特判开头一段 就过原题了。
分析一下性质。可以发现进位的序列有如下性质:
- 在任何一段时间内,两个数的进位次数相差不超过 。
证明:按位与的进位操作不改变差,可以不考虑。对位数归纳,可以发现如果下一位进位次数差不超过 ,加上这一位开头结尾的差也不超过 ,那么不可能多进两位。
因此进位操作一定是 和 交错,中间放入一些 。
然后考虑操作带来的影响。可以发现一次操作后,无论开头状态是多少,一个交错段中间都会合并为 段,只有两侧可能留下来一些单点。而一个 段可能不变,但如果开头有数则它变为一个 交错的段(每次操作后直接进位)
可以发现合并是 合 ,因此总合并次数有限,合并可以暴力做。后面部分即使留下来一些单点,可以发现下一次操作也是相邻两个单点合并起来,中间变成一个交错段,因此猜想段数也很少,那么如下实现应该能过:
用 deque 维护每一个 段,支持整体加标记,交错部分暴力维护,如果遇到 段时当前状态为 则整体 跳过,否则暴力拆开。
猜测这东西可能是线性的,但是根本不会证。读者可以自行尝试证明或证伪。
还可以猜想后面的段也不需要特殊维护,因为后面操作几次就合并了。事实上只维护开头的 段可以通过原数据,但有明显特殊情况:如果开头是一个 接一串 ,当前位初始是 ,那么这一位后不改变进位序列的状态。因此 差很多的全 可以卡掉这种维护。但只维护第一段似乎有可能是对的(读者自行尝试)
正经的 Sol 2:
直接按照时间依次维护整个串显然是不可行的,之前的做法相当于换为从低位到高位的顺序,这样的正确性是较为显然的。
再考虑换一种顺序。因为加法只向前影响,因此另一种思路是从高往低位做。显然做一次操作时可以从高往低逐位做。
考虑能不能把顺序换过来,可以发现如下结论:
- 可以将操作顺序换为,每次做能操作的最高位直到当前位不能再操作或者达到 时刻(每次 进位需要 时间)。具体来说,找出最高的一对 ,向前操作 次或者直到这对 还存在,然后向低位继续该操作。这样不改变答案。
证明:高位每次操作向前进位时一定会留下一位 ,低位无论怎么进位每次最多把一个 填上(加一次只能进一位),那么低位不可能追上高位,因此高位操作不会受低位操作影响。
考虑这样进行操作,可以发现上述结论同时表明当前 向前操作时不会再遇到 ,那么只有如下类型的操作:
- 遇到 ,则用 个时刻向前进位(相当于操作进位)。
- 遇到 ,则这一位变成 ,然后进位的值变为 (相当于加法进位)。那么接下来如果遇到 ,则会停在这里,遇到 则将当前位变为 继续进位,遇到 则变回进位 的情况继续。
注意到如果在第二种情况下还能继续操作,那么最后一步一定把某一位上的一个 变成了 ,进一步可以发现如果这里操作了 次(显然 ),则会去掉 个 (除去第一步不会外,中间的进位也会删一个 )。因此简单的势能分析可以发现第二种情况的操作次数是 的。
但第一种操作直接做显然是 的,因此考虑跳过连续的 段。简单的实现方式是维护所有非全 的位,或者维护前面所有 中 所在的位置。这样即可跳过一段,但需要注意只能走到中间的情况。
一种实现方式是从高到低维护所有 位置,只维护前面的位置。然后操作时扫过的 全部弹出,最后再重新放进去。
复杂度
Code
第一种就不放了.jpg
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 2005000
int k,n,m;
int s0[N],c0,s1[N],c1;
int t0[N],r0,t1[N],r1;
char s[N],t[N];
int main()
{
scanf("%d%d%d",&n,&m,&k);
scanf("%s%s",s+1,t+1);
for(int i=1;i*2<=n;i++)swap(s[i],s[n-i+1]);
for(int i=1;i*2<=m;i++)swap(t[i],t[m-i+1]);
s0[0]=s1[0]=1e8;
for(int i=1e6;i>=1;i--)
{
int u=s[i]=='1',v=t[i]=='1';
if(u&&v)
{
int nw=i,re=k,v0=1,v1=1;
while(v0+v1)
if(v0&&v1)
{
int nt=min(s0[c0],s1[c1]);
if(nw+re<nt)nw+=re,s0[++c0]=nw,s1[++c1]=nw,re=v0=v1=0;
else
{
re-=nt-nw;nw=nt;
if(s0[c0]<s1[c1])c0--,t1[++r1]=nw,v1=0;
else c1--,t0[++r0]=nw,v0=0;
}
}
else if(v0)
if(s0[c0]==nw+1)
{
if(s1[c1]==nw+1)t1[++r1]=nw+1,c1--;
c0--;nw++;
}
else if(s1[c1]==nw+1)c1--,v1=1,nw++;
else s0[++c0]=nw+1,v0=0;
else
if(s1[c1]==nw+1)
{
if(s0[c0]==nw+1)t0[++r0]=nw+1,c0--;
c1--;nw++;
}
else if(s0[c0]==nw+1)c0--,v0=1,nw++;
else s1[++c1]=nw+1,v1=0;
while(r0)s0[++c0]=t0[r0--];
while(r1)s1[++c1]=t1[r1--];
}
else if(u)s0[++c0]=i;
else if(v)s1[++c1]=i;
}
s0[c0+1]=s1[c1+1]=0;
for(int i=1;i<=c0;i++)
{
printf("1");
for(int j=1;j<s0[i]-s0[i+1];j++)printf("0");
}
printf("\n");
for(int i=1;i<=c1;i++)
{
printf("1");
for(int j=1;j<s1[i]-s1[i+1];j++)printf("0");
}
}
AGC024D Isomorphism Freak
Problem
定义一棵树的权值为:考虑以每个点为根得到的有根树,权值为有根树同构意义下这些有根树的种类数。
现在给一棵 个点的树。可以进行任意次如下操作:
向树上某个点加一个叶子。(可以在加入的点下再加叶子)
可以任意进行操作,在最小化权值的基础上再最小化树最后的叶子数量。求出两个最小值。
Sol
考虑答案的下界。进一步地,因为操作可以加叶子,尝试找一个不随着加叶子而减小的下界(反例是度数的种类数)。更进一步还希望同构加叶子能达到该下界(反例是 )。
从同构判定的角度考虑,可以发现有根树同构则必定相同的量中有一个最大深度。众所周知任意取点为根时最大深度点一定在直径取到(否则可以从直径最长的性质推出矛盾)。进一步分析可以发现,设直径长度为 ,则取直径上的点可以发现最大深度为 (认为根节点深度为 )的有根树都能出现。另一方面其它点为根的最大深度不可能小于下界(走到直径上后取较长的部分),也不可能大于上界(直径性质)。那么权值至少是 。
考虑这能不能达到。考虑以直径中点(点或边)为根,则要达到这个当且仅当一层内每个点为根的有根树都同构。那首先每一层的点度数必须相同(同构的一个条件),因此每一层每一个点的儿子数量相同。进一步可以发现只要满足这个条件,显然树是完全对称的,每一层为根的所有树显然同构。
然后考虑往原树(以直径中点为根)中加叶子,可以发现每个点的儿子树只能增加,因此考虑将每一层的儿子数量设为原本这一层所有点儿子数量的最大值,此时一定可以将原树嵌入扩展后的树(扩展后每个点的所有子树对称,任意分配儿子继续做即可,由于取 了一定可以分配),那么原树一定可以加叶子加到当前树。
因此可以达到下界,且如果确定了直径中点所在的点或边,则叶子数量为每一层所有点儿子数量最大值的乘积(中点是边则额外乘 )。这可以确中点后 求出。
但可能原树中多个位置都能成为最后的直径中点,因此考虑枚举所有情况求出叶子个数的最小值。
可以发现如果直径长度为偶数(原中点在边上),则往两侧移深度一定变大,那么此时只有一种情况。如果直径长度为奇数,则可以发现除去中点外,中点相邻的边也可以是最后的直径中点(答案不变)。此时直接枚举复杂度为 ,但也可以做到更优:考虑算出每个子树内每个深度的最大儿子数量,再算出整体的。枚举一个子树相当于将这个子树从整体中删去,再改变高度合并进来,那么这只会影响子树深度层的结果,预处理整体中每个深度的最大值和来自其它子树的次大值即可 维护。这样复杂度可以降到 ,但也没有必要。
更暴力的方式是枚举每个点和每条边,这样显然可以以 通过。
最后考虑答案的大小。显然极端情况是一侧一条链,另一侧一条链上每个点挂若干个儿子。可以发现这大概相当于将 分成若干个数,然后每个数减一乘起来,求最大值。可以发现最优解大概是分成 ,而 只有 级别,因此答案显然不超过 ,可以直接维护。如果将高精度放入考虑,则之前的复杂度都需要乘一个 。
Code
#include<cstdio>
using namespace std;
#define N 105
int as1=1e8,n,head[N],cnt,in[N],dep[N],a,b,s[N][2];
long long as2;
struct edge{int t,next;}ed[N*2];
void adde(int f,int t){ed[++cnt]=(edge){t,head[f]};head[f]=cnt;ed[++cnt]=(edge){f,head[t]};head[t]=cnt;in[f]++,in[t]++;}
void dfs(int u,int fa){dep[u]=dep[fa]+1;for(int i=head[u];i;i=ed[i].next)if(ed[i].t!=fa)dfs(ed[i].t,u);}
void solve1(int x)
{
dfs(x,0);
int mxdep=0;long long as=1;
for(int i=1;i<=n;i++)if(dep[i]>mxdep)mxdep=dep[i];
for(int i=1;i<mxdep;i++)
{
int mx=1;
for(int j=1;j<=n;j++)
if(dep[j]==i&&mx<in[j]-1)mx=in[j]-1;
if(i==1)mx++;
as*=mx;
}
if(as1>mxdep||(as1==mxdep&&as2>as))as1=mxdep,as2=as;
}
void solve2(int x,int y)
{
dep[y]=0;dfs(x,y);dep[x]=0;dfs(y,x);dep[x]=1;
int mxdep=0;long long as=2;
for(int i=1;i<=n;i++)if(dep[i]>mxdep)mxdep=dep[i];
for(int i=1;i<mxdep;i++)
{
int mx=1;
for(int j=1;j<=n;j++)
if(dep[j]==i&&mx<in[j]-1)mx=in[j]-1;
as*=mx;
}
if(as1>mxdep||(as1==mxdep&&as2>as))as1=mxdep,as2=as;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)scanf("%d%d",&s[i][0],&s[i][1]),adde(s[i][0],s[i][1]);
for(int i=1;i<=n;i++)solve1(i);
for(int i=1;i<n;i++)solve2(s[i][0],s[i][1]);
printf("%d %lld\n",as1,as2);
}
AGC024E Sequence Growing Hard
Problem
给定 ,求有多少组整数序列 满足如下限制:
- 所有元素在 之间
- 的长度为
- 是 的子序列
- 字典序大于
答案对给定模数取模。
Sol
计数的目标和字典序的限制都是对序列整体要求的,但显然不能将整个序列作为状态,也很难存在特殊的状态设计。
可以发现中间两个限制说明 是在 上插入一个元素得到的,那么考虑将整个过程换为每次在序列中插入一个数。但这样仍需要解决两个问题:如何描述字典序的限制以及如何去重(去除插入后得到相同序列的情况)
首先考虑字典序的限制。假设 插入在了 的前面,那么 显然合法, 显然不合法。而如果 ,则需要继续向后比较,可以发现比较方式为找到之后第一个不是 的数,比较它和 的大小。但问题是这个限制不太局部。
同时考虑去重,可以发现将同一个数插入不同位置可能导致重复的情况只有将 插到一段连续 内的不同位置(考虑每一个极长相同段的长度)。那么考虑钦定只能插入在一段末尾(插入到中间和结尾相同),即不能将 插入到 前面。这样完成了去重。
可以发现这样的去重同样解决了字典序限制的问题:合并两个限制后,形式变为在一个数前面插入一个大于它的数或者插入在结尾。这样正好变为了一个局部的限制。
考虑把结尾的操作和之前统一形式。考虑在序列结尾插入一个 ,那么在结尾插入和在之前插入形式一样。那么问题变为如下形式:
初始序列中只有一个 。进行 次操作,每次在序列的一个数前面插入一个严格更大但不超过 的数,求方案数。
考虑如何描述这些插入。考虑将插入看成有向边:每个元素看成一个点,将 插入在 前面看成 向 连边。那么每次操作相当于给一个点加一个在最右侧的叶子,序列即为有根树的后序遍历。
那么转而考虑有根树的问题。可行的树是所有 个点,根权值(元素的值)为 ,儿子权值大于父亲权值且儿子有序的树。此时还需要考虑插入得到一棵树的方案数。第一次操作只能从根开始扩展,但因为之后只能插入在根的最右侧,那么第一次必须插入最后最左侧的儿子。可以发现这之后最左侧儿子的部分和树的其余部分就独立了,那么方案数是剩余两部分方案数的乘积,再乘上两侧操作按顺序归并的组合数。
考虑将权值放入 dp,设 表示 个点的子树,根节点权值为 的方案数,答案为 。像之前那样枚举根最左侧的子树,可以得到:
前缀和优化第二个求和即可。复杂度
Code
#include<cstdio>
using namespace std;
#define N 305
int n,m,p,dp[N][N],su[N][N],c[N][N];
int main()
{
scanf("%d%d%d",&n,&m,&p);n++;m++;
for(int i=1;i<=m;i++)dp[1][i]=1,su[1][i]=i;
for(int i=0;i<=n;i++)c[i][i]=c[i][0]=1;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j-1]+c[i-1][j])%p;
for(int i=2;i<=n;i++)
{
for(int k=1;k<=m;k++)
for(int j=1;j<i;j++)
dp[i][k]=(dp[i][k]+1ll*dp[i-j][k]*su[j][k-1]%p*c[i-2][j-1])%p;
for(int k=1;k<=m;k++)su[i][k]=(su[i][k-1]+dp[i][k])%p;
}
printf("%d\n",dp[n][m]);
}
AGC024F Simple Subsequence Problem
Problem
给定 ,定义 为所有长度在 间的 串构成的集合。
现在给出一个 的子集 以及一个 ,找到最长的满足如下条件的 串 。如果有多个最长串,则求出字典序最小的:
是 中至少 个字符串的子序列。
Sol
这里称一个串的权值为 中包含它作为子序列的串数量。
如果从长度和字典序的角度出发,则首先需要求出一个长度的串中权值的最大值,然后如果逐位确定,经过分析可以发现相当于还是需要做一个上述形式的问题。但求最大值这个问题还是很难做,大概是因为比较两个可重集的结果是困难的。(事实上这里逐位确定的做法可以直接推出正解)
从字典序出发没有更好的解决方式,事实上可以看出因为逐位确定不能剪枝,还是需要算出所有权值。因此考虑直接把每个 的权值算出来,这样显然能回答询问。
考虑从 的角度出发,分析一个串能转移到哪些子序列。这里是计数问题因此需要保证不算重。那么最经典的方式是贪心匹配(子序列自动机):判定 是否是 的子序列只需要依次考虑 的每一位,看能否匹配 需要匹配的下一个字符。这也可以看成对于 的每一位,跳到 中当前位置向后该字符第一次出现的位置。那么可以使用如下方式不重复地找到 的所有子序列 :维护 当前剩余的后缀。每次向 末尾加一个字符,然后在后缀中找到当前字符第一次出现的位置(不出现则不能加这个字符),然后删掉这一段继续做。
但现在有 个串,显然不能对每个串做一遍。但注意到整个过程中需要的状态只有剩余后缀 和已经填的 ,因此考虑将不同串的这一过程合并,只记录这两个状态。可以发现 ,长度和为 的状态只有 个,枚举 总状态也只有 个。转移只需要找到接下来第一个 或 ,可以预处理或者用一些位运算操作做到 。
另一种思考方式是直接从 角度出发。考虑放入 的第一位,用之前的贪心处理 中的每一个串,删掉一段前缀或者直接去掉这个串。那么这样操作后 变成了一个可重集,但除此之外放入 剩余部分的过程和之前相同。那么求出一个 的权值只需要依次放入后操作 。最后一个串的权值是操作结束后的 。
中相同的串可以合并(只记录出现次数),此时可以发现状态仍然是 已经填的部分和 剩余的后缀,那么状态数总和还是 的,转移可以类似 dfs,从 转移到 即可,实现时只需要记录当前 的状态。这样复杂度仍然是 ,空间复杂度甚至只有 。事实上这样实现就等价于开头提到的的逐位确定。但我懒得实现了
Code
#include<cstdio>
using namespace std;
#define N 23
int dp[1<<N][N],n,k,v[N],nt[N][2],ct,le,lg[1<<N];
char s[1<<N];
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<=n;i++)
{
scanf("%s",s);
for(int j=0;j<1<<i;j++)
dp[j|(1<<i)][0]=s[j]=='1';
}
for(int i=2;i<1<<n+1;i++)lg[i]=lg[i>>1]+1;
for(int j=(1<<n+1)-1;j>=0;j--)
{
int le=lg[j];
for(int k=1;k<=le;k++)v[k]=(bool)(j&(1<<k-1));
nt[le][0]=nt[le][1]=-1;
for(int k=le-1;k>=0;k--)
{
nt[k][0]=nt[k+1][0];nt[k][1]=nt[k+1][1];
nt[k][v[k+1]]=k+1;
}
for(int k=0;k<=le;k++)
if(dp[j][k])
{
dp[j&((1<<k)-1)][k+1]=dp[j&((1<<k)-1)][k+1]+dp[j][k];
if(nt[k][0]!=-1)dp[(j&((1<<k)-1))|(j>>(nt[k][0]-1)<<k)][k+1]=dp[(j&((1<<k)-1))|(j>>(nt[k][0]-1)<<k)][k+1]+dp[j][k];
if(nt[k][1]!=-1)dp[(j&((1<<k)-1))|(j>>(nt[k][1]-1)<<k)][k+1]=dp[(j&((1<<k)-1))|(j>>(nt[k][1]-1)<<k)][k+1]+dp[j][k];
}
}
for(int l=n;l>=1;l--)
for(int j=0;j<1<<l;j++)
if(dp[j][l+1]>=k)
{
for(int i=l;i>=1;i--)printf("%d",(bool)(j&(1<<i-1)));
return 0;
}
}
AGC023D Go Home
Problem
一辆车上有 组人,第 组人有 个,他们的目的地是 ,保证目的地两两不同。
车初始在位置 ( 不是任何一个目的地)。每个时刻,所有人会投票决定车前进的方向(向右()还是向左()),如果投票打平则车向后前进,然后车会向这一方向移动 的距离。此时如果有人到达了目的地,他会下车。
所有人投票的策略都是最小化自己下车的时间,求所有人最优操作下最后一个人下车的时间。
Sol
首先任意时刻下车的人构成一段区间(以下车位置排序)。设当前这个区间为 ,则下一组人下车时状态只可能是 (此时车开到了 的位置)或者 (此时车开到了 的位置)。那显然每组人只会选对自己最优的一个方向直到到达下一个状态,而不会反复横跳。那么这里不需要每一个时刻都判断一次,而是可以每当一组人下车时判断一次。
但通过样例可以发现策略是不显然的:如果左侧有一组 个人且右侧有两组分别 个人,此时如果向左,则顺序显然是 ,但如果向右, 下车后只剩两个人,此时一定会先 再 ,可以发现此时 都用了更多时间。那么最右侧的一组人实际上会在一开始投票向左移动。
根据之前的分析有 naive 的 做法:记录每个状态下所有人到达的时间,但这显然没有前途。因此考虑进一步分析。
根据之前的分析显然需要倒着讨论,那么从最后的情况开始分析。显然如果剩余所有人都在车的某一侧,那么所有人都会选择这一侧,这种情况容易解决。考虑接下来最简单的情况,即两侧各有一组人。如果只有这两组人,那显然车会先向人较多(根据规则打平时左侧获胜)的一侧开,然后再回到另一侧。
考虑稍微复杂一点的情况。如果在人多的那一侧中间再加一些人,那很容易猜想车仍然会先往这个方向开到底。这一点直接归纳就能证明,但仍然意义不大。考虑往人少的一组一侧中间加人(即样例的情况),根据样例这样的情况是不平凡的。不妨设情况为左侧一组人比右侧一组人多,然后右侧中间又放了一些人。此时车的前进方式一定是向右一段,再向左,再向右(但可能有一段向右不存在)。考虑最右侧一组人的决策。显然他的最优状态是车向右开到底,再向左开。但可以发现这是不可能的:即使车像这样开到了右侧倒数第二组人,最后两侧只剩一组人时左侧人更多,因此会向左。那么可以发现最右侧一组人不可能比最左侧一组人先到。
可以发现这个结论对一般情况也成立,即:
不妨设 且 (另一种情况类似)。如果车当前在 之间,则第 组人一定在第 组人之后到达。此时显然第 组人到达的时间是第一组人的时间加上 。
证明:如果不是这种情况,那么第 组人下车前左侧还有若干组,但右侧只有 ,且 。那么在 下车前用之前的第一种情况分析可以发现此时车会向左开,因此矛盾。
那么如果 ,那么第 组人尽早到达实际上等价于第 组人尽早到达。反之同理。因此可以发现如下结论:
如果 ,则第 组人在第 组人到达之前一定和第 组人投相同的票,且 一定最后到达。
那么考虑将第 组人合并到第 组人上,求出操作后的答案。此时唯一的问题在于第 组下车后情况不同,但此时车只会向右,因此情况不变。那么求出变换后车的移动后,将车再移动到 即可得到原问题的移动方式。
那么考虑一直这样合并,直到某一时刻所有剩余位置都在车的一侧(上述讨论的边界),然后即可还原出车移动的过程(人下车的顺序),这样即可得到答案。
复杂度 ,注意细节和边界情况。
Code
#include<cstdio>
using namespace std;
#define N 100500
long long v[N],su[N],n,s,as,l,r,tp[N],ct;
int main()
{
scanf("%lld%lld",&n,&s);l=1,r=n;ct=n;
for(int i=1;i<=n;i++)scanf("%lld%lld",&v[i],&su[i]);
while(l<r)
{
if(s<v[l])su[l]=1e16;if(s>v[r])su[r]=1e16;
if(su[l]>=su[r])su[l]+=su[r],tp[ct--]=v[r],r--;else su[r]+=su[l],tp[ct--]=v[l],l++;
}
tp[1]=v[l];tp[0]=s;
for(int i=1;i<=n;i++)as+=tp[i]>tp[i-1]?tp[i]-tp[i-1]:tp[i-1]-tp[i];
printf("%lld\n",as);
}
AGC023E Inversions
Problem
给定 和长度为 的序列 。对所有满足 的 阶排列 求和其逆序对数。答案模
Sol
数逆序对的经典操作是拿线性性拆成每一对 ,因为逆序对没有什么整体上的性质。
在本题中考虑线性性,即对于每一对 ,求和满足 且满足 的排列数量。那么问题转到数排列上。
可以发现如果没有 的限制,数 的部分是一个经典问题:考虑将 排序然后从小到大填,那么之前可以填的数被之后可以填的包含,从而前面怎么填不影响之后的方案数。记 为 排序后的序列,则总方案数即为 。另一种对称的方式是考虑每个数填到哪,从大到小填。那么记 表示 的数个数,那么答案是 。两种方式完全对称,这里采用后一种。事实上前一种也可以解决问题,但会多加入一点细节。
现在考虑加入 的限制,naive 的方式是枚举 ,这样在之前的 上将某些段减一或减二再去掉两项即可得到新的方案数。分析一下甚至可以 求出一对 的答案:在 的二维平面上考虑问题,限制相当于一条从左下到右上且只向右向上的折线,在折线下填排列(所以这甚至是个棋盘问题)。此时方案数可以在折线上求出:每次向右时乘以 即可。枚举两个取值相当于删掉两行两列,考虑所有 的取值相当于删的列固定,删的行满足某些限制。那么考虑折线上 dp,状态设为之前删了哪些行哪些列。然后考虑一次处理多个行,问题是需要保证逆序对的顺序关系。那么考虑分治:求出 的答案,然后向下分治。但一个问题时做一次是 的,虽然可能选的列只有区间长度个,但行可能有很多。此时可以考虑以可以选的列为分界点,可以发现中间每一段只需要知道这一段里面(在前面选了若干行列的情况下)里面选若干行的所有情况的权值和。那么可以分治时大力合并,从而做到区间长度的复杂度。这样即可做到 ,但看起来不好写。然后我又懒了,留作练习
枚举两个数的取值还是过于复杂,考虑有没有更好的方式。分析一下可以发现一些简单的对称性:考虑所有 的排列,显然这些排列中交换 不影响限制,而这之后 就反转了,因此由对称性这部分的贡献是方案数的一半。而在剩下的情况中, 较大的位置的取值大于了另一个 ,此时大小关系完全确定。那么通过这一转化,原问题变为了两个更加简单的问题。
考虑 的情况,此时第二部分一定不是逆序对,那么只需要算第一种情况,这相当于把某个 减小后算 的方案数。此时的问题比之前容易:将一个 减小直接相当于将一段 减一(这就体现出用 的好处,这里用 会涉及到把一个元素放过去,但也能做)。记 ,那么一段减一的影响相当于在原方案数上乘上 段内的 。(如果用 ,则相当于一段 后移再移动一个数,这会带来很多细节)
顺序枚举 ,考虑加入,可以发现相当于维护序列 (下标是值域),支持如下操作:
- 单点
- 给定 ,询问
那可以直接线段树。如果 则可以直接 BIT:将 的一段乘积拆为两个前缀乘积的商。但这里 可以是 (如果本来这一项是 ),简单的处理方式是对于每个 找到向左第一个 ,然后询问只从这开始,连乘也从上一个 之后开始,这样就避免了 的影响,然后就可以 BIT 了。
现在考虑 的情况,此时不仅有第一种情况,还有 的部分,此时一定是逆序对。那么需要求 的排列数量。下界难以直接处理,因此考虑容斥,然后下界就变成了两个上界相减,那么就是没有这个下界的方案数减去要求 的方案数。可以发现后者和第一种情况是重复的,事实上也可以放在一起:考虑在一开始就翻过来,变成用总数减去 的方案数,那么这就变成了之前的情况,答案是额外满足 排列数量的一半,即原答案是这个的一半。那么需要做的事和之前一样,但 的顺序反转了,从而整个过程都反转了。用另一个 BIT 类似维护即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 300500
#define mod 1000000007
int n,p[N],su[N],v[N],su1[N],as,as1,tr[N],lb[N],tr2[N],rb[N],tr3[N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
void add(int x,int k){for(int i=x;i<=n;i+=i&-i)tr[i]=(tr[i]+k)%mod;}
int que(int x){int as=0;for(int i=x;i;i-=i&-i)as=(as+tr[i])%mod;return as;}
void add2(int x,int k){for(int i=x;i<=n;i+=i&-i)tr2[i]+=k;}
int que2(int x){int as=0;for(int i=x;i;i-=i&-i)as+=tr2[i];return as;}
void add3(int x,int k){for(int i=x;i<=n;i+=i&-i)tr3[i]=(tr3[i]+k)%mod;}
int que3(int x){int as=0;for(int i=x;i;i-=i&-i)as=(as+tr3[i])%mod;return as;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&p[i]),su[p[i]]++;
for(int i=n-1;i>=1;i--)su[i]=su[i+1]+su[i]-1;
as=1;
for(int i=1;i<=n;i++)as=1ll*as*su[i]%mod;
if(!as){printf("0\n");return 0;}
lb[0]=1;
for(int i=1;i<=n;i++)
{
v[i]=1ll*(su[i]-1)*pw(su[i],mod-2)%mod;
su1[i]=v[i];if(su1[i-1])su1[i]=1ll*su1[i]*su1[i-1]%mod;
}
for(int i=n;i>=1;i--)if(v[i+1])rb[i]=rb[i+1];else rb[i]=i;
for(int i=1;i<=n;i++)if(v[i-1])lb[i]=lb[i-1];else lb[i]=i;
for(int i=1;i<=n;i++)
{
int su11=que(p[i])-que(lb[p[i]]-1),su2=que2(p[i])-que2(lb[p[i]]-1);
as1=(as1+1ll*as*su11%mod*su1[p[i]]%mod*(mod+1)/2)%mod;
su11=que3(rb[p[i]])-que3(p[i]),su2=que2(n)-que2(p[i]);
as1=(as1+1ll*as*su2%mod-1ll*as*su11%mod*pw(su1[p[i]],mod-2)%mod*(mod+1)/2%mod)%mod;
add(p[i],pw(su1[p[i]]?su1[p[i]]:1,mod-2)),add2(p[i],1);add3(p[i],su1[p[i]]);
}
printf("%d\n",(as1+mod)%mod);
}
AGC023F 01 on Tree
Problem
给一棵 个点的有根树,每个点有 中的一个权值。
你需要将所有点排成一列,要求如果 是 的祖先,则 比 在排列中先出现。
定义一个排列方式的权值为:依次写出排列中每个点的权值得到一个 序列,权值为序列中子序列 10
出现的次数。
求出所有合法排列中的最小权值。
Sol
用这题稍微讲讲 exchange argument 现在是复读集训队论文时间
首先考虑没有树上限制的问题,那显然 放前面 放后面。
考虑一个稍微复杂的情况:每个位置有 个 和 个 ,那么排成一列的代价是 ,求此时排成一列的代价。
这个的做法也是众所周知的:考虑交换相邻两个数,可以发现 时 排在前面更优。这个条件等价于 ,那么按照 从小到大排序即可得到一组最优解。(证明将在下面给出)
考虑该做法的本质:这里首先找到了一个序关系(这里是 的大小),然后通过序关系排序即可得到最优解。
首先考虑序关系的本质。这里给出一个该做法的充分条件:
定义一种序关系 ,如果 (这里 是两个元素),则在任意存在相邻 的序列中,对于一对相邻 ,将 放在前面一定不比将 放在前面差。(这里不是当且仅当,因为还需要考虑序关系的基本限制)
同时还需要满足基本的全序性质:
- 传递性:如果 ,那么
- 完全性:, 或者
- 自反性:
此时可以证明,如果存在这样的序关系,则将所有元素排序即可得到原问题的最优解。
证明:设排序后的结果为 。如果最优解为 ,考虑通过若干次交换回到 :首先将 移到开头,然后将 移到第二个位置,以此类推直到还原。可以发现这样每一次交换时,后面的元素一定小于前面的元素(因为比它小的都到最前面去了,同时由传递性),因此每次交换后结果不会变差。这说明 不比任何一组解差。
典型存在序关系的问题有这里的 (顺序是 ),有归并若干序列 最大化前缀和最小值(很多例子,顺序是 ,第一类内部按照 从小到大,最后一类按照 从大到小,可以证明这是对的),还有字符串拼接最小化字典序(显然比较关系是 ,可以证明这是个序关系,但是较为复杂)。
需要注意的是这里的序关系是在整体中交换两个位置,而不是只有这两个元素的情况下交换。虽然大部分情况下等价,但也有不等价的情况,例如例子 。
但到这里的内容都是众所周知的。接下来考虑本题中的变化,即有一棵有根树,树上的祖先关系限制了祖先必须比自己先选。
在满足一些额外限制的情况下,树上的这一问题同样可以在 次比较内解决。这里给出一个充分条件:
不仅可以对元素找出一个满足上述条件的序关系,还可以对元素组成的序列找出序关系。且如果 ,那么任意序列中这两段相邻时, 放在前面不会变差。
接下来有两个基于该性质的 做法。可以发现很多情况下序列类似于一个元素,因此元素的关系可以推出序列的关系:字符串拼接显然还是字符串; 合并起来只需要直到前缀最小值和总和,那么还是一个类似的 ;通过交换可以发现 合并后在比较时等价于一个 。同时这样也排除掉了一些显然不能在树上做的问题:例如定义权值为前 个数之和,如果只比较元素那显然是从大到小排序,但可以发现一些序列是无法比较的:例如 和 且 。如果左边没有数那么 严格更优,但左边有一个数则 严格更优,因此不存在序关系。事实上这个问题如果上树则不弱于 卷积,但它目前还不存在次数低于 的做法。
这里有两种经典做法。第一种做法基于如下结论:
记 为除去根上元素外当前最小的元素所在的点,那么存在一组最优解, 的父亲的下一个为 。
证明:考虑交换,如果不满足这一条件,可以发现 父亲到 中间这部分都可以扔到 后面去而不改变合法性。那么考虑一步一步进行交换,因为 是最小的,每次向前交换都不会使答案变大,这就得到了结论。
那么此时选到 的父亲下一个一定是 ,因此考虑将这两个点合并为一个点,选它的表示依次选两个数。那么此时变为之前的问题,只是一个点上对应的可能是一个序列而不仅仅是一个元素。但根据加强后的条件,序列间也可以进行比较,因此之前的分析仍然成立。那么使用堆维护除去根外的所有元素,每次找到最大的向上合并即可。合并可以使用树上并查集。
这一过程也可以还原出方案:记录每次合并的情况即可。但因为这个代价很好计算:只需要维护当前序列内部代价和内部 ,也可以直接算出答案。只需要 次比较。
第二种做法则是从下往上考虑每个子树的子问题。首先考虑一条链的情况,此时有如下结论:
如果 只有一个儿子 ,且 上元素 上元素,则存在一种最优解,选了 后下一个为 。
证明:还是考虑不满足条件的情况。因为 没有其它儿子,因此中间这一段可以被扔到前面去,也可以被扔到后面去。那么考虑 上元素和中间部分序列作比较,因为 ,那么要么中间 ,要么 中间,否则不满足序关系。那么第一种情况对应扔到前面不差,第二种情况对应扔到后面不差。
因此继续合并,可以发现一条链一定可以合并到一条满足前一个元素(序列) 后一个元素的链。然后考虑一般树,可以发现如下结论:
任意一个子树也可以转化为一条满足上述条件的链。
证明:归纳,如果子树都满足了,则当前树变为根上连出去若干条链,每条链满足前一个元素 后一个元素。但此时如果不考虑根,也不考虑这些链的限制,直接选的话根据之前结论这些链上的元素会从小到大排列,即使有树上其它部分它们在内部没有限制的情况下也会从小到大排列。但这样它们自动满足了链的限制。可以发现把它们的限制删去,即变为所有点连到子树根后最优解不会变大(这之后的任意方案可以转化为满足原限制的不变差的方案)。进一步可以发现把它们归并为一条链最优解不会变小(存在之前的最优解满足这一性质),那么现在整个子树变成了一条链,子树内都是按顺序排列的,但根可能比下面的元素大。因此再用之前的结论,在根上一直合并直到它 下一个元素,这样就满足上述条件了。(这里严谨证明细节较为复杂,因此省略了不少细节)
那么一个子树只需要维护变为这样的链后所有元素构成的集合即可。根据证明中的构造,向上合并的方法是将子树的集合合并,然后不断将根和集合中最小的元素按顺序合并为序列,直到某次合并后根的元素 集合中最小元素。
直接启发式合并比较次数为 ,但高超的平衡树合并好像可以做到 次。同时这个做法还可以求出每个子树的答案。
可以发现本题属于之前的第一种例子:可以将 看成 , 看成 ,然后就是一个树上的这类问题,用上述任意一种做法即可。
复杂度 ,但看起来两种 都没有 的启发式合并快。
Code
做法一的实现:
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
#define N 200500
#define ll long long
int n,a,f[N],fa[N];
ll as;
struct sth{ll a;int b,c;}sr[N];
bool operator <(sth a,sth b){ll tp=a.a*b.b-a.b*b.a;if(tp)return tp>0;else return a.c<b.c;}
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)scanf("%d",&f[i+1]);
for(int i=1;i<=n;i++)fa[i]=i;
set<sth> f1;
for(int i=1;i<=n;i++)
{
scanf("%d",&a);a^=1;
sr[i]=(sth){a,1,i};
if(i!=1)f1.insert((sth){a,1,i});
as+=a;
}
as-=1ll*as*(as+1)/2;
for(int i=1;i<n;i++)
{
sth v1=*f1.begin();f1.erase(v1);
int nt=finds(f[v1.c]);
if(nt!=1)f1.erase(sr[nt]);
fa[finds(v1.c)]=nt;
as+=sr[nt].b*v1.a;sr[nt].a+=v1.a;sr[nt].b+=v1.b;
if(nt!=1)f1.insert(sr[nt]);
}
printf("%lld\n",as);
}
做法二的实现(启发式合并):
#include<cstdio>
#include<set>
using namespace std;
#define N 200500
long long as=0;
int n,head[N],cnt,is[N],id[N],a;
struct sth{int a,b;friend bool operator <(sth a,sth b){if(1ll*a.a*b.b==1ll*a.b*b.a)return a.a<b.a;return 1ll*a.a*b.b<1ll*a.b*b.a;}};
multiset<sth> st[N];
struct edge{int t,next;}ed[N*2];
void adde(int f,int t){ed[++cnt]=(edge){t,head[f]};head[f]=cnt;ed[++cnt]=(edge){f,head[t]};head[t]=cnt;}
void dfs(int u,int fa)
{
id[u]=u;
for(int i=head[u];i;i=ed[i].next)
if(ed[i].t!=fa)
{
dfs(ed[i].t,u);
if(st[id[ed[i].t]].size()>st[id[u]].size())id[u]^=id[ed[i].t]^=id[u]^=id[ed[i].t];
for(set<sth>::iterator it=st[id[ed[i].t]].begin();it!=st[id[ed[i].t]].end();it++)
st[id[u]].insert(*it);
}
sth tp1=(sth){is[u],!is[u]};
while(!st[id[u]].empty())
{
if(tp1<*st[id[u]].begin())break;
sth tp2=*st[id[u]].begin();st[id[u]].erase(st[id[u]].find(tp2));
as+=1ll*tp1.a*tp2.b;
tp1.a+=tp2.a;tp1.b+=tp2.b;
}
st[id[u]].insert(tp1);
}
int main()
{
scanf("%d",&n);
for(int i=2;i<=n;i++)scanf("%d",&a),adde(a,i);
for(int i=1;i<=n;i++)scanf("%d",&is[i]);
dfs(1,0);
int su1=0;
while(!st[id[1]].empty())
{
sth tp2=*st[id[1]].begin();st[id[1]].erase(st[id[1]].find(tp2));
as+=1ll*su1*tp2.b;su1+=tp2.a;
}
printf("%lld\n",as);
}
AGC022D Shopping
Problem
有一条长度为 的线段。有一辆火车在这条线段上来回走:它在时刻 到达左端点 ,在时刻 到达右端点 。
线段上有 个地点,第 个地点在位置 ,你需要在这个地点连续停留 个时间。
你在 时刻在位置 ,你可以不花费时间在车到达所在位置时上车,也可以在任意给定地点(包括左右端点)下车。但你只能通过火车移动。
求完成所有地点的停留要求,且回到左端点所需要的最少时间。
Sol
考虑一个地点上的情况。显然在一个地点停留的时间只和进入和离开的方向有关。那么考虑决定每个点进入和离开的方向,再还原路径。
直接还原路径有一些问题:还原的路径可能存在从端点绕回来的情况,但这样就要考虑端点绕了多少次,使问题变得复杂。但可以发现从端点绕相当于从一个点向一个方向出发后,绕回这个点再向另一个方向(如果绕回之前到达目标,那显然不如绕到端点之前就到)。那么这不如直接换出发方向,在决定方向时处理这种情况。这样还原路径时不再需要考虑这种情况。
那么问题可以看成,一列上有 个点,你需要从最左侧出发经过每个点正好一次,移动有 的代价,在每个点停留的代价和进入离开的方向有关,那可以得到一个 的做法(cf704b):维护前缀部分的路径构成了多少段,但这里无法通过。
分析一下这里的权值。如果进入离开的方向相同,则时间是 的某个倍数(大于等于 的第一个倍数),否则是 的某个倍数加上另一个方向折返(例如向右进入向左离开则是右侧折返)的距离(也是大于等于 的第一个)。此时可以发现 增加 时所有方向经过该点的时间都增加 ,那么考虑如果某个 ,则将其 ,在最后的答案上额外增加 。这样变为所有 的情况。( 和 也可以转化,但 会带来额外的讨论,因此这里全部转化为 )。
那么此时进入离开方向相同时的代价就是 。对于从右侧向左在这里折返的情况,如果 则代价是 ,否则是 。另一方向同理( 换成 )。为了形象表示,这里用 <
表示从右侧过来折返,>
表示从左侧过来折返,.
表示不折返。重新描述之前的暴力做法,相当于中间的每个点可以决定选择 .<>
中的一个。限制为左端点必定是 <
(因为从这里出发);除去右端点前的最后一段外,每一段前面 <
必须严格多于 >
(因为必须一条路径经过每个点)。可以发现后者等价于,且最后两个位置(最后一个地点和右端点)中至少有一个 >
,且删去最后一个 >
和开头 的 <
后剩余 <>
部分构成合法括号序列。
代价是每个位置选择的代价加上每一段长度乘上前面 <
的个数减去 >
的个数。限制是括号序列。那可以发现限制可以看成括号配对,代价计算也可以看成括号配对:每一对的代价是两侧括号选择的代价加上两倍中间的距离。同时每个选 .
还有 的代价(右端点除外)。最后的限制相当于要求开头的 <
必须和最后一个 >
配对,且最后一个 >
一定在最后两个位置中。
结合之前分析的权值考虑配对的代价,可以发现配对部分是 可能加上 ,再配合中间距离的代价可以发现正好全部消掉,那么可以发现如果两边都不额外加 则配对代价为 。那么如果不考虑开头结尾,可以发现中间配一对 的代价会使总代价减少 ,但其它配对都不能减少总代价( 就和不配对一样了),因此中间只会配 的对,即左侧一个能做到 的和右侧一个能做到 的配对。
然后考虑开头结尾的限制。如果左右端点 <>
配对,则需要 的代价。但如果左端点能和第 个点这样配对且第 个点取 >
不需要额外 代价,则同样可以 配对且解决了第 个点的限制而没有额外代价(因为右端点可以不配对而没有代价)。进一步分析可以发现不用代价拿掉一个点一定不会变差:即使最优方案里面它可以和中间配对,配对的 代价也和删去它后一个单点的代价相同。而另一方面,如果第 个点取 >
有额外代价,那这里配对不优,同时它在中间部分的分析中也不会配对。那么可以发现如下性质:
如果第 个点可以取 >
而没有额外代价,即 ,则答案为 加上前 个点配对的结果。否则它也不可能参与前面的配对,答案为 加上前 个点配对的结果。
那么只需要再看中间配对的问题,每个点取 <
和 >
是否有 的额外代价等价于能否以这种字符参与匹配,即如下问题:
个位置排成一列,每个字符可以取 <>
中的一个子集,前面的 <
可以和后面的 >
可以匹配,求最多配多少对。然后答案为 减去 乘配对数。
然后考虑求最大匹配,可以随便写出一些贪心,例如如下过程:
顺序考虑,维护当前前面有多少没匹配的 <
以及有多少已经匹配的 <>
中右端点可以改成 <
。此时一个 <
直接加入,一个 >
可以和 <
匹配,也可以和之前的第二种情况交换,然后换出来的位置改为选 <
。那么贪心能匹配就匹配,否则尝试换。
它看起来是很对的,但是我不大会证。
但本题中的问题有更好的性质:分析一下条件,可以选 <
当且仅当 ,可以选 >
当且仅当 。那么可以发现 时不存在只能选 <
的, 时不存在只能选 >
的。那么只能选单个的形如 >>..><<..<
,因此这部分内部不能匹配,它们只能和 <>
都能选的配对,或者都能选的两两配对。因此显然应该先把只能选单个的贪心和 <>
都能选的配对,此时左右独立,每部分可以贪心就近选(有很多贪心方式),然后剩下的都能选的任意两两配对。这个性质也可以直接说明之前的贪心可以得到正确答案。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 300500
int st[N],v[N],s[N],n,l;
long long as;
int main()
{
scanf("%d%d",&n,&l);as=l*2;
for(int i=1;i<=n;i++)scanf("%d",&s[i]);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
for(int i=1;i<=n;i++)
{
as+=(1ll*v[i]+l*2-1)/(l*2)*l*2;
v[i]=(v[i]-1)%(l*2)+1;
if(v[i]<=s[i]*2)st[i]++;
if(v[i]<=(l-s[i])*2)st[i]+=2;
}
int l1=0,l2=0;
for(int i=1;i<n;i++)
if((st[i]&2)&&l1)l1--,as-=l*2,l2+=st[i]&1;
else if((st[i]&2)&&l2)l1++,l2--,l2+=st[i]&1;
else l1+=st[i]&1;
if((st[n]&2))as-=l*2;
printf("%lld\n",as);
}
AGC022E Median Replace
Problem
称一个长度是奇数的 01
串是好的,当且仅当可以通过如下操作将其变成单个字符 1
:
选择一个长度为 的子串,将其替换为子串三个字符的中位数。
现在给定奇数 和一个长度为 包含 01?
的字符串,求有多少中将 ?
替换为 01
之一的方式使得串是好的。答案模
Sol
考虑怎么判断一个串是否合法。有一些 naive 的角度,例如直接考虑 01
的个数然后贪心用 000
来减少。但这样会遇到很多问题,例如 00100
。从而这里需要更细致的分析。
考虑记录每个 0
段的长度。这样可以得到一个序列 ,依次表示每两个 1
间 0
的数量(以及开头到第一个 1
和结尾)。那么操作可以看成如下几种之一:
- 操作
101
: 删掉非边界的一个 。 - 操作边界上的
01*
:删掉边界的一个 。 - 操作其它非
000
与111
:合并两个加起来不为 的数 ,变为 。 - 操作
000
:将一个 的数 。 - 操作
111
:删去连续的两个 。
合法当且仅当能操作到全 。
进行一些分析。首先可以发现合并 也等价于删去 ,那么关于 的操作都等价于删去它。所以可以考虑先删掉所有 ,且之后操作出的 默认删掉。问题是这样可能出现空串,但可以发现空串应该对应不合法(因为单个 对应 0
),那么只需要将合法条件变为同时至少有一个 。
然后可以发现 一定是不差的:考虑对总和和长度归纳,直接套用 之前的操作,唯一的问题出在合并 和 ,但上一步说明合并 得到的 也可以删去,那么等效。
那么一直 再去除 ,此时序列只剩下 。可以发现操作相邻 可以将其变为 ,然后 再消去。操作 也可以看成直接消去。那么问题变为,有一个 序列,每次可以消去两个相邻元素,求能否只剩下 且至少剩下一个 。
考虑删相邻的 ,此时如果剩余的 比 多就显然成功,否则情况只能是 或者 ,但归纳可得这些情况一定不行,那么做之前的判定即可。
但这个判定比较复杂,难以计数。考虑简化判定。首先考虑 部分,一个 naive 的做法时每次找到第一个 和后一个消掉,那么这样一定是先处理了相邻的 ,然后再把 和 消掉,因此这样做是对的。进一步地,可以发现开头有两个 显然就直接合法了:先任意操作最后,最多再拿一个 去合并。那么可以扫过去,当前最多保留一个 和一个 (等待和后面消)。
回顾整个过程,首先 一定不差,那么 000
可以先直接做。然后 段可以直接删,那么遇到 101
或者 01
开头都可以直接操作。最后说明了 段一定贪心向后合并,即 001
也可以直接操作。此时可以发现任何一种 段都可以被这样的贪心覆盖。
那么也可以考虑顺序扫。类似之前的结论,可以发现开头是 11
就直接合法了,这样状态数就有限了:
0 |
1 |
|
0 |
00 |
01 |
1 |
10 |
11 |
00 |
0 (000 可以直接做) |
0 (001 的情况) |
01 |
0 (01 开头) |
1 |
10 |
100 |
1 ( 段的情况) |
100 |
10 (000 的情况) |
10 (001 的情况) |
11 |
11 (已经可以合法) |
11 |
可以发现这就够了。合法终止状态为 1
和 11
。这也相当于找了一个 个状态的自动机。(事实上直接编出贪心是不难的,但证明是不容易的)
此时计数的 dp of dp 就容易了:状态记为考虑了前 位,当前自动机状态为 的方案数。
复杂度 ,常数 。
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 300500
#define mod 1000000007
int n,dp[N][8],tr[8][2]={1,2,3,4,5,7,1,1,1,2,6,2,5,5,7,7};
char s[N];
int main()
{
scanf("%s",s+1);
n=strlen(s+1);dp[0][0]=1;
for(int i=1;i<=n;i++)for(int j=0;j<2;j++)if(s[i]!='0'+!j)
for(int k=0;k<8;k++)dp[i][tr[k][j]]=(dp[i][tr[k][j]]+dp[i-1][k])%mod;
printf("%d\n",(dp[n][2]+dp[n][7])%mod);
}
AGC022F Checkers
Problem
数轴上有 枚棋子,初始时棋子 的位置是 。接下来进行如下操作 次:
按顺序拿走任意两枚棋子,设拿走的棋子坐标为 ,则放回一枚坐标为 的棋子。允许有棋子位置相同。
最后只会剩下一枚棋子,求这枚棋子所在的位置有多少种可能。答案模
Sol
考虑每个棋子对最后坐标的贡献系数。倒着考虑,最后的系数是 。每次操作考虑这次操作得到的棋子是啥,相当于从系数中拿出一个 ,再放回 。 次后得到的就是初始每个位置的贡献系数。
那么可以发现贡献系数的形式是 ,其中 。又因为坐标是 ,那么 显然不可能进位到 级别,从而可以发现:
最后的坐标不同当且仅当存在一个位置的贡献系数不同。
然后考虑贡献系数。正着考虑回去,可以发现一组系数合法当且仅当它可以通过如下 次操作只剩下一个 :
拿出两个数 ,放回一个 。
可以发现这和贡献系数的顺序无关,那么判定合法性时只需要考虑每种 出现了多少次。但坐标显然和顺序有关,因此方案数最后需要乘上每一种数放在一起排列的方案数,即一个多重组合。
考虑判定合法性,即考虑一组系数能不能被还原到 。考虑是否可以让操作方式满足某些特性。注意到操作 不会加入大小级别为 的数,那么可以发现:
如果有解,则存在一种操作方式,依次进行 的操作,即按照绝对值从大到小操作。
证明: 更小的操作产生的数对 严格更大的操作一定没有用。那么可以不断交换顺序按照从大到小排序,每一步都将一个 小的放到一个大的后面,这样每次交换后都合法。
那么判定合法可以从大到小考虑:先操作删掉所有 ,然后做 ,直到 。
考虑一个 上的操作,那么只有 受到影响。设有 个 , 个 ,那么它们相当于 次将 变为 ,以及 次将 变为 。设初始有 个 和 个 ,考虑能完成操作的条件。首先显然需要操作后数有 个,即 。同时可以发现如果 则至少需要一个数进行第一步操作,即此时 。可以发现这就充分必要了:先用一个元素做 轮相互抵消的变换,然后做单种变换。根据不等式限制第二部分一定能做出来。
那么考虑从后往前 dp,首先需要记录后面选了多少个数(因为总共选 个数)。然后直接按照上面的思路需要记录后面部分操作完后 分别的个数。但可以发现中间的操作都只用到了 ,唯一需要的地方实际上是最后需要只留下一个 。那么可以状态只记录 ,转移枚举这一层 分别的个数,然后求出是否可以操作以及操作后的个数向后转移,最后一层算答案时需要特判。
如果把层数也记下来,则状态数 复杂度 ,因为常数小可以通过。但也可以发现层数没有实际意义,因此可以考虑按照选数的数量顺序转移,判断是否是最后一层只需要看总和即可。这样复杂度变为 。
Code
#include<cstdio>
using namespace std;
#define N 53
#define mod 1000000007
int n,c[N][N],dp[N][N*2],as;
int main()
{
scanf("%d",&n);
for(int i=0;i<=n;i++)c[i][0]=c[i][i]=1;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++)c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
dp[0][n]=1;
for(int i=0;i<n;i++)for(int j=0;j<=n*2;j++)if(dp[i][j])
{
for(int p=0;i+p<=n;p++)for(int q=0;i+p+q<=n;q++)if(p+q)
{
int np=p+j-n,nq=q+n-j;
if(np<0||nq<0)continue;
int vl=1ll*dp[i][j]*c[i+p][p]%mod*c[i+p+q][q]%mod;
dp[i+p+q][n+np-nq]=(dp[i+p+q][n+np-nq]+vl)%mod;
if(i+p+q==n&&np==1&&!nq)as=(as+vl)%mod;
}
}
printf("%d\n",as);
}
AGC021E Ball Eat Chameleons
Problem
有 只变色龙和红蓝两种颜色的球,变色龙按照如下方式变色:
初始为蓝色,每当其吃下一个球时,如果当前其吃下的红色球和蓝色球数量不同,则其变为吃下数量更多的那种颜色;如果相同则不变色。
现在有 个球,每个球可以任意取两种颜色之一。求有多少种决定颜色的方式满足:
将 个球依次扔进去,每次扔进去后球会被任意一只变色龙吃掉,吃掉后再扔下一个。存在一种吃的方式使得最后所有变色龙变为红色。
答案模 。
Sol
首先考虑一只变色龙的情况。其变色只和两种颜色的大小关系有关,因此考虑将红色看成 ,蓝色看成 。此时吃掉一个球相当于加上对应的数,变色可以看成如果当前数 则变红, 则变蓝,否则不变。
考虑一个序列导致的最终颜色。首先考虑最后一步,可以发现如果总和 则是红色,总和 则是蓝色。而如果总和 ,可以发现上一步操作结束时总和一定 ,那么由最后一位可以倒推出上一步结束时的总和进而确定颜色。因此可以发现,最后的颜色为红色当且仅当:
- 序列总和 ,或者
- 序列总和 且最后一个数为 (空序列不行)。
可以发现除了最后一个位置,限制只和总和有关。
考虑原问题,一个串合法等价于它可以被划分为 个满足上述条件的子序列。如果只考虑总和,那么并不需要关心子序列的顺序,因此可以任意往里面加元素和拿出元素。考虑对方案调整使得方案满足某些性质。首先可以发现:
- 如果有解,则存在一组最优解,其中总和为 的子序列数量尽量小。
证明:如果不是最小,显然可以从一个总和 的序列中拿出一个 放入总和为 的。因为总和 后就没有限制了,所以操作后仍然合法。
记原序列总和为 ,则显然至少需要 个和为 的序列。因为 后没有额外限制,可以发现只需要把这 个和为 的划分出来,剩余部分显然能划分好:先让每个位置和为 ,再将剩余的全部放入某一个即可。那么只需要考虑划分这些部分。但如果存在一个和 的序列,则这些部分也可以将元素扔到它里面,唯一的问题是结尾不能扔掉,进而可以发现:
- 如果 ,则合法当且仅当可以划分出 个
证明:首先显然每个和为 且满足条件的序列存在 作为子序列(因为 在结尾),另一方面划分这么多 后剩余部分显然可以合法。
那么先考虑 的情况,枚举 ,问题相当于求 个 和 个 能划分出至少 个 的方案数。
首先考虑一个 序列能划分出多少个,这显然可以贪心匹配。进一步分析(或者根据括号匹配的经验)可以发现这个值和最小前缀和有关:记最小前缀和为 ,则正好有 个 无法匹配。证明是容易的:考虑每次最小前缀和降低的时候,此时前面一定没有可用的 了;而最小前缀和不变的时候因为前面放弃了这么多 ,因此一定能匹配。
那么问题变为:求有多少个 个 和 个 的序列最小前缀和不小于某个值。那么从 考虑,这可以看成从 每次向右上和右下走,最后走到 且不经过某条 的方案数。那这显然是一个翻折问题:考虑算经过了 的方案数,在第一次到达这条线时翻折后面的路径,显然翻折后变为到 的路径,且翻折前后显然是双射(可以发现翻折前后和 的第一个交点位置不变),那么不合法的方案数是另一个组合数,两个相减即可。那么 情况的某个 可以 预处理后 求出。
最后考虑 的情况,此时每个序列总和都为 ,因此不能直接用之前的讨论。但可以发现此时每个划分出的子序列末尾都必须是 ,那么原序列末尾是 。考虑删掉这个 ,那么方案中它原本所在的序列就变为了一个和为 的合法序列。可以发现这就变为了 减一且 的情况。可以发现原问题合法当且仅当删去最后一个 后前面合法:正方向考虑删去元素所在的序列显然成立,反方向考虑将 加入原先唯一一个和为 的序列即可。那么这种情况同样变为一个 情况的方案数。这样就做完了。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 500500
#define mod 998244353
int n,k,fr[N],ifr[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int calc(int n,int k)
{
if(k<0||k>n)return 0;
return 1ll*fr[n]*ifr[n-k]%mod*ifr[k]%mod;
}
int main()
{
scanf("%d%d",&n,&k);
fr[0]=1;for(int i=1;i<=k;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[k]=pw(fr[k],mod-2);for(int i=k;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
for(int i=k;i*2>k;i--)
{
int rs=i*2-k;if(rs>n)rs=n;
rs=k-i-(n-rs);if(rs<0)continue;
as=(as+2ll*mod+calc(k,i)-calc(k,i+rs+1))%mod;
}
if(k%2==0)
{
int rs=k/2-n;
if(rs>=0)as=(as+2ll*mod+calc(k-1,k/2)-calc(k-1,k/2+rs+1))%mod;
}
printf("%d\n",as);
}
AGC021F Trinity
Problem
给定 。对于一个 的 01
矩阵,考虑如下序列:
- 表示第 行第一个
1
出现的位置(如果不存在则为 ,接下来同理) - 表示第 列第一个
1
出现的位置 - 表示第 列最后一个
1
出现的位置
考虑所有可能的矩阵,求出不同的 的数量,答案模
Sol
求不同情况数的常见操作是判断情况合法条件以直接去重,或者尝试按某种顺序搞并消除后面没有处理的部分对前面的影响。但这里可以发现判断合法也需要按顺序做,因此考虑后者。
考虑尽量避免后面对前面的影响,因此考虑找到某种顺序填数。可以发现列上有两个限制,行只有首位的限制。因此考虑每次往最后加一列,这样几乎不会对之前的造成影响,唯一的问题是它可能把之前 的行变成非零,其它之前确定的非零 以及任意 都不会再改变了。
考虑规避掉这一点。可以发现唯一的问题出在空行上,那么考虑直接删掉空行,每次加入一列时可以填一些行进来,这样解决了 的问题。问题是加空行可能改变 ,但可以发现这不改变两种方案的 是否相同,那么删空行也不会影响这部分。
因此考虑设 表示 行 列没有空行时不同的 的数量。显然答案就是 ,那么可以先不考虑计算答案。=。根据之前的分析,考虑加一列转移。设这次填上了 个空行,那么空行和原来的行之间可以任意排列( 可以区分两者)。然后考虑这一列的 的方案数。首先考虑一种排列新加入行和原来行的方案。这一列上新加入的行必须填 1
,其它行可以任意。为了简便这里将新加入的行记作 1
,之前的行记作 0
。那么如果有至少一个新加入的行,填 的方案数就是第一个 1
左侧行数加一,填 的方案数类似,因此总系数是两者相乘。而如果没有新加入行,那相当于任意填,容易发现情况数是 (枚举有 个 1
或者至少有两个 1
)。
现在考虑整体 的系数,考虑处理掉原先的贡献系数。那么最经典的做法是组合意义:如果 ,可以发现在第一个 1
左侧插入一个 2
,再在最后一个 1
右侧也插入一个 2
的方案数就等于需要的值。那么相当于将 归并起来,要求所有 1
在两个 2
之间。可以发现这里可以将 2
也换成 1
,因为可以通过开头结尾把它们区分出来。那么问题变回了 01
归并,可以发现方案数是 。但 时答案还是 。
转移系数是组合数,此时考虑 的转移,可以发现特判 的转移后组合数显然可以拆成一个卷积的形式。那么考虑暴力卷积,复杂度 ,可以通过。
但还有更深刻的做法。考虑维护 的生成函数,不考虑 操作相当于卷积 ,而 时 ,那么只需要卷积后减去
卷积都是组合数,考虑用 EGF 表示来处理阶乘:如果卷积系数是 ,写成 EGF 的话就解决了 的系数问题,那么只需要卷积 。考虑这里的情况。,写成 EGF 后还差 ,那么考虑卷积 ,对着 的式子算一下可以发现这是 。但卷积后还需要给 的系数乘上 ,这也是经典的问题:考虑求导凑出系数,因为求导可以做到第 项乘 。具体来说这里只需要 就能做到。类似地求导处理要减去的部分,可以得到如下 EGF 意义下的转移:(这里 )
考虑 的形式,操作只有乘 和求导。那么可以发现 一定是一些 的线性组合,其中 ,直接归纳就能证明这一点。那么考虑维护这些 的系数。转移直接按照上述定义即可。这样就能 得到 的形式。
但还需要通过这些 得到答案。算答案时要求 ,显然不能直接做。但可以发现乘 这部分相当于 EGF 下再卷积一个 (最基本的形式),然后求 项系数。那么只需要考虑求 的 项系数,因而显然只需要求 的 项系数,但这显然是 。注意这里是 EGF 所以最后还需要乘 。
最后算答案看起来需要阶乘,但可以发现和前面 展开的阶乘抵消后只有 项,那么可以做到几乎与 无关(只有快速幂需要)。复杂度
Code
Sol 1:
#include<cstdio>
using namespace std;
#define N 16400
#define M 204
#define mod 998244353
int dp[M][N],n,m,fr[N],ifr[N],rev[N],a[N],b[N],ntt[N];
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int c(int a,int b){if(a<b)return 0;return 1ll*fr[a]*ifr[b]%mod*ifr[a-b]%mod;}
void dft(int s,int *a,int t)
{
for(int i=0;i<s;i++)rev[i]=(rev[i>>1]>>1)|(i&1?s>>1:0),ntt[rev[i]]=a[i];
for(int i=2;i<=s;i<<=1)
{
int st=pw(3,(mod-1)/i);
if(t==-1)st=pw(st,mod-2);
for(int j=0;j<s;j+=i)
for(int k=j,t=1;k<j+(i>>1);k++,t=1ll*t*st%mod)
{
int v1=ntt[k],v2=1ll*ntt[k+(i>>1)]*t%mod;
ntt[k]=(v1+v2)%mod;ntt[k+(i>>1)]=(v1-v2+mod)%mod;
}
}
int inv=t==-1?pw(s,mod-2):1;
for(int i=0;i<s;i++)a[i]=1ll*ntt[i]*inv%mod;
}
int main()
{
scanf("%d%d",&n,&m);
dp[0][0]=1;
fr[0]=ifr[0]=1;for(int i=1;i<=8020;i++)fr[i]=1ll*fr[i-1]*i%mod,ifr[i]=pw(fr[i],mod-2);
for(int i=1;i<=m;i++)
{
int l=1;while(l<=n*2)l<<=1;for(int j=0;j<l;j++)a[j]=b[j]=0;
for(int j=0;j<=n;j++)a[j]=1ll*dp[i-1][j]*ifr[j]%mod;
for(int j=1;j<=n;j++)b[j]=ifr[j+2];
dft(l,a,1);dft(l,b,1);for(int j=0;j<l;j++)a[j]=1ll*a[j]*b[j]%mod;dft(l,a,-1);
for(int j=0;j<=n;j++)dp[i][j]=(1ll*a[j]*fr[j+2]+1ll*(1ll*j*(j-1)%mod*499122177%mod+j+1)%mod*dp[i-1][j])%mod;
}
int as=0;
for(int j=0;j<=n;j++)as=(as+1ll*dp[m][j]*c(n,j))%mod;
printf("%d\n",as);
}
Sol 2:
#include<cstdio>
using namespace std;
#define N 205
#define mod 998244353
int n,m,f[N][N],g[N][N],h[N][N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
scanf("%d%d",&n,&m);
f[0][0]=1;
for(int i=1;i<=m;i++)
{
for(int j=0;j<=i;j++)for(int k=0;k<=i;k++)g[j][k]=h[j][k]=0;
for(int j=0;j<i;j++)for(int k=0;k<i;k++)
{
g[j][k+1]=(g[j][k+1]+f[j][k])%mod;
g[j+1][k]=(g[j+1][k]+mod-f[j][k])%mod;
g[j][k]=(g[j][k]+mod-f[j][k])%mod;
h[j+1][k]=(h[j+1][k]+mod-f[j][k])%mod;
}
for(int j=0;j<=i;j++)for(int k=0;k<=i;k++)
{
h[j][k]=(h[j][k]+1ll*k*g[j][k])%mod;
if(j)h[j-1][k]=(h[j-1][k]+1ll*j*g[j][k])%mod;
}
for(int j=0;j<=i;j++)for(int k=0;k<=i;k++)
{
f[j][k]=(f[j][k]+1ll*k*h[j][k])%mod;
if(j)f[j-1][k]=(f[j-1][k]+1ll*j*h[j][k])%mod;
}
}
for(int i=0,tp=1;i<=m&&i<=n;tp=1ll*tp*(n-i)%mod,i++)for(int j=1;j<=m;j++)as=(as+1ll*f[i][j]*pw(j+1,n-i)%mod*tp)%mod;
printf("%d\n",as);
}
AGC020D Min Max Repetition
Problem
给定 ,你需要将 个 A
和 个 B
排成一个字符串,满足如下条件:
- 定义一个字符串的权值为其中最长的连续相同字符子串长度。首先要最小化字符串的权值。
- 在最小化权值的基础上,最小化字符串的字典序。
此时显然只有唯一解。求出这个解的第 位。
多组询问,
Sol
首先考虑最小权值应该是多少。考虑用一种元素分隔另一种,那么容易发现最小值的下界是 。显然这个下界是可以达到的:如果 那么交替放就做到了 。否则不妨设 ,那么可以先用 个 B
将所有 A
分开,再均匀地加入 B
,这样正好做到 的权值。
此时的一种常见方法是对着最小值分析,但这里确定最小权值后可以发现还有很大的自由度:例如如果 ,那么权值是 ,但此时空余了 个位置,那么答案开头甚至可以有很多 A
。因此这样不能接受。
设要求的最小权值为 。考虑直接分析最小字典序的限制。为了字典序最小,显然的开头是 AA..AB
循环,其中开头有 个 A
。那么最小字典序的方案一定是一直循环这个,直到某个位置再放 A
就不存在权值 的解了。
考虑接下来部分的情况。这部分中再拿一个 A
到开头去就不合法了,那么后面部分一定有 ,同时任意一种合法方案一定不存在两个 A
相邻(不然可以拿一个 A
到前面去)。此时开头只能是 B
,要最小化字典序首先要最小化当前这段 B
的长度。那可以发现最优解满足接下来的每一段都是 ABB...B
,其中有 个 B
,因为即使这样开头也会留下 B
。那么这就是字典序最小的答案。
最后唯一的问题在于如何求出分界点。显然的做法是二分前面有多少个 A
,可以做到 询问。但也可以不二分:注意到 A
减少的更快,那么判断后面是否合法只需要 。需要注意的是如果前面填到 ...AB
,那后面计算的时候需要将这个 B
带上,因为它也会加入接下来的第一段 B
。那么考虑先求出有多少段 AA..AB
,然后求出下一段能有多少 A
,每一步都相当于解一个不等式。(其实二分的过程也是这样,但是用二分解不等式)。那么即可 得到答案的结构。
复杂度
Code
#include<cstdio>
using namespace std;
int T,a,b,k,s,l,r;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d%d%d",&a,&b,&l,&r);
if(a>b)k=(a-1)/(b+1)+1;else k=(b-1)/(a+1)+1;
//(b-t)+1 <= k((a-kt)+1) -> (k^2-1)t <= k(a+1)-b-1
int li=k==1?b>a?0:1e9:(1ll*k*(a+1)-b-1)/(1ll*k*k-1);
if(li<0)li=0;if(li>b)li=b;if(li>a/k)li=a/k;
//(b-t) <= k(a-kt-s+1) -> k*s <= k(a-kt+1)-(b-t)
int l2=(1ll*k*(a-k*li+1)-b+li)/k;
if(l2<0)l2=0;if(l2>k)l2=k;
s=li*(k+1)+l2;
for(int i=l;i<=r;i++)
if(i<=s)printf("%c",i%(k+1)?'A':'B');
else printf("%c",(a+b+1-i)%(k+1)?'B':'A');
printf("\n");
}
}
AGC020E Encoding Subsets
Problem
考虑如下 01
串 的编码方式:
- 单个字符
0
,1
可以对应编码为0
,1
。 - 如果 可以分别被编码为 ,那么 可以被编码为 。
- 如果 可以写为 重复 次,且 可以编码为 ,那么 可以编码为
(
+ +x
+)
,这里 表示 的十进制表示。
定义一个 的权值为它不同的编码数量。
定义 是 的子集,当且仅当 且 的每一位都不大于 的对应。现在给出一个长度为 的包含 01
的字符串,求其所有子集的权值和。答案模
Sol
首先考虑怎么算一个串的答案。可以发现编码的过程相当于不断对当前某个 01
子串用 操作变为 (...xk)
的形式。从括号的角度可以发现按照括号匹配,再通过 x
划分就可以还原出所有的操作,因此这个编码是可逆的。同时可以发现操作顺序是没有意义的(里外先合并和后合并不影响结果)
考虑编码后的第一位,如果它是 01
那么之后的 操作一定不影响第一位,否则这里一定是 (
。在一种编码中根据括号匹配可以找到对应的 )
,进一步原串的长度可以被确定(操作可还原)。因此考虑枚举这个 (
对应一段的长度 ,再枚举 ,那么合法当且仅当前 位存在长度为 的循环节。可以发现接下来这个长度为 的部分和剩余后缀独立。那么容易得到一个区间 dp:设 表示当前串 子区间的方案数,然后第一种情况直接转移,第二种情况枚举 转移。
现在考虑原问题。直接的想法是直接扩展之前的 dp:设 表示一个串的答案,对于第一种情况,可以发现如果第一位是 1
那么这种转移有两种方案,否则只有一种。对于第二种情况,枚举 后要求这 部分相同,那么可能的情况就是将这 个串的子集交起来得到的结果。可以发现交起来还是一个这样的子集:因为限制可以看成 上界,合并限制直接对应位取 即可。那么定义 表示同时满足两个串 子集限制的串可以表示为 的子集,那么类似之前的方式可以发现:
但因为 ,现在就没有子串的性质了,因此状态数可能很多,但是:
Believer’s way. Run your solution on cases of maximum length with different number of ones and see that it is very fast.
直接记忆化,复杂度看起来是 ,但是能过,甚至不需要什么卡常小技巧。(显然最多递归 次,那么复杂度甚至是 ,但这对于这么小的 没有意义)
Code
#include<cstdio>
#include<vector>
#include<map>
using namespace std;
#define mod 998244353
map<long long,int> tp;
char s[105];
vector<int> st[105];
int dp(vector<int> s);
int solve(vector<int> s)
{
int sz=s.size(),as=0;
if(sz==1)return 1+s[0];
int s2=st[sz].size();
for(int i=0;i<s2;i++)
{
int v=st[sz][i];
vector<int> vl;
for(int j=1;j<=v;j++)
{
int mx=1;
for(int k=j-1;k<sz;k+=v)mx&=s[k];
vl.push_back(mx);
}
as=(as+dp(vl))%mod;
}
return as;
}
int dp(vector<int> s)
{
int sz=s.size();
if(!sz)return 1;
if(sz==1)return 1+s[0];
long long vl=-1;
if(sz<=50)
{
vl=0;
for(int i=0;i<sz;i++)vl=vl*2+s[i];
vl=vl*64+sz;
}
else vl=sz+(1ll<<60);
if(tp[vl])return tp[vl];
int as=0;
for(int i=0;i<sz;i++)
{
vector<int> s1,s2;
for(int j=0;j<=i;j++)s1.push_back(s[j]);
for(int j=i+1;j<sz;j++)s2.push_back(s[j]);
as=(as+1ll*solve(s1)*dp(s2))%mod;
}
if(vl!=-1)tp[vl]=as;
return as;
}
int main()
{
for(int i=1;i<=100;i++)for(int j=i*2;j<=100;j+=i)st[j].push_back(i);
scanf("%s",s+1);vector<int> st1;
for(int i=1;s[i];i++)st1.push_back(s[i]-'0');
printf("%d\n",dp(st1));
}
AGC020F Arcs on a Circle
Problem
有一个长度为 的环,同时有 条线段,第 条线段长度为 。所有给定数均为整数。
将所有线段独立随机放到环上(实数随机),求环上每个位置都被覆盖的概率,输出实数,精度误差 。
Sol
处理实数的常见操作是转而枚举大小关系: 间随机 个实数,存在数相同的概率为 ,所有 种大小顺序出现的概率均为 。
但这里涉及到了长度。虽然也可以枚举顺序:考虑枚举 个端点的顺序,但枚举顺序后又带来了很多类似 的限制。 维下的半平面交是困难的。
输入都是整数,那么限制也是整数。考虑将整数部分和小数部分分开:整数部分正常考虑,这就变成了有限的情况。而因为所有线段长度都是整数,因此小数部分不受影响,只需要枚举小数部分的大小关系即可。
枚举 种可能的小数关系,这样问题变为了一个类似取值整数的情况:设 个小数部分为 ,将环分成 个长度为 的段,然后在每一段内找到 对应的点。问题就变为有 个点,第 条线段可以覆盖某个 到接下来第 个 的一段,这样就只有有限个取值了。同时每种取值的概率都是 (再乘上 )
那么考虑正常的 dp,但环上问题难以直接处理,通常的想法是破环为链。但这里显然不能任意选一个点断开,因为跨过这个点的线段情况可能是 种。常见的处理是找一个特殊位置:这里考虑找到最长的一条线段的开头分开,那么之前的线段不可能越过最长线段去覆盖后面的,这样就避免了后面对前面的影响。
从最长线段的左端点开始,考虑按照之前的 个点转移:状态设为当前考虑到第 个点,当前向右能覆盖到第 个点,之前已经用掉了哪些下标的线段时前面的方案数。枚举当前点是否向右连出线段即可,限制为每一个点之后都能覆盖到下一个点。
可以发现在环上只关心 之间的顺序,并不关心它们和 的顺序,那么枚举只用枚举 种(另一个解释是从最长的 开始,只需要枚举剩下的顺序)。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 7
#define M 52
int n,l,d[N],dp[N*M][N*M][1<<N],p[N];
double as;
int main()
{
scanf("%d%d",&n,&l);
for(int i=1;i<=n;i++)scanf("%d",&d[i]),p[i]=i;
sort(d+1,d+n+1);
do{
for(int i=0;i<=n*l;i++)for(int j=0;j<=n*l;j++)for(int k=0;k<1<<n-1;k++)dp[i][j][k]=0;
dp[0][d[n]*n][0]=1;
for(int i=1;i<n*l;i++)for(int j=i;j<=n*l;j++)for(int k=0;k<1<<n-1;k++)if(dp[i-1][j][k])
{
if(i%n&&!(k&(1<<p[i%n]-1)))dp[i][min(n*l,max(j,i+d[p[i%n]]*n))][k+(1<<p[i%n]-1)]+=dp[i-1][j][k];
if(i<=j)dp[i][j][k]+=dp[i-1][j][k];
}
as+=dp[n*l-1][n*l][(1<<n-1)-1];
}while(next_permutation(p+1,p+n));
for(int i=1;i<n;i++)as/=i*l;
printf("%.15lf\n",as);
}
AGC019E Shuffle and Swap
Problem
给定两个长度为 的 01
串 。它们 1
的个数相同,记这个值为 。
记 为 中所有 的下标,类似定义 为 中所有 的下标。进行如下操作:
随机重排 ,然后依次对于每一个 ,交换 和 。(如果 则不交换,但这样合法)
求操作后 相等的方案数(总共 种重排方式),答案模
Sol
显然位置间的顺序不影响结果(因为所有操作都是交换两个位置),同时全 0
位置显然没有意义,因此输入实际上相当于两个数:有 个位置前后都是 1
,有 个位置之前是 1
之后是 0
。(那么对称地有 个位置之前是 0
之后是 1
)接下来用 表示一个初始是 ,最后是 的位置。那么 里面是所有 的位置, 里面是所有 的位置。
此时有至少两种方式分析问题。第一种方式是按照操作顺序考虑。考虑第一次操作。枚举这次操作在 中分别选择了哪一类,那么有四种情况:(这里交换 指 是 中的位置, 是 中的位置)
- 交换 和 ,这显然合法。可以发现这样操作并没有影响其它位置的状态,那么这直接变为 的子问题。可以发现两边分别选一个位置方案数是 。
- 交换 和 ,这显然不合法,因为 只有这一次交换机会。
- 交换 和 ,可以发现是合法的。这之后 位置合法且没有其它操作了。而之前的 位置值变成了 ,值需要变回 且之后只剩一次 中的操作,可以发现这个状态正好等价于一个 位置的状态。那么这变为了 的子问题,方案数是 。
- 交换 和 。显然合法,但问题是这样之后一个 的位置只剩一次操作,但这不能被一种原先位置的状态表示。
特殊分析最后一种情况。考虑在两个都没操作过的 间进行交换带来的影响。考虑它们的下一次交换。显然这次也只能和一个当前是 1
的位置交换(因为它们只剩下一次操作),但这样的话它就不可能和 交换,因为后者只有一次操作机会,和 交换会使得 不合法,和 交换会使得自己不合法。那么它们只能继续和 交换,同时这些 之前也不能和 交换过(不然根据之前的分析 会变为和 等价)。那么可以发现如下结论:
考虑所有 位置间交换涉及到的位置,这些位置一定和其它位置在操作上独立。
证明:不断用之前的方式扩展,可以找到若干个环把操作连起来,然后就和外面独立了。
那么枚举有 个这样的位置,则位置有 种,内部操作的方案数是 ,这些操作和其它操作归并的方案数是 。接下来变为 且不包含之前最后一种操作时的方案数。那么可以直接按照上面的式子递归,记 为这样的方案数,那么显然有
复杂度 ,可以通过。但也可以继续优化:
考虑 部分的转移, 每减一乘以刚才的 ,可以发现这部分最后的贡献系数一定是 。同理 的转移中 的系数也可以拿出来变为 。这样转移变为 。考虑每次乘的 可以发现这相当于对所有长度为 ,所有元素在 间的不降序列,求和序列所有项乘积。从生成函数角度考虑可以发现这相当于 。考虑暴力分治+FFT算 ,这样复杂度是 的。但把系数翻转过来就是 ,那么这是标准的斯特林数形式,有简单的倍增做法:考虑先求低 项,从低项到高项只需要将 带入多项式,而带入 的情况展开后可以发现是一个卷积的形式:
把组合数拆开容易发现是一个 和 的差卷积。那么倍增单次复杂度 ,总复杂度也是 。最后求逆即可得到原答案,复杂度
另一种分析问题的方式是先不考虑操作顺序,从操作的配对入手。考虑在每一对 间连一条有向边 ,那么有 种连边方式,每种连边方式又有 种操作所有边的顺序。
考虑固定一种方式后看操作顺序中有多少合法。因为每个点入度出度不超过 ,因此可以发现图一定由若干有向环和有向链构成。且有向环和链的中间点一定是 位置,链由一个 位置开头,到一个 位置结尾。显然一个环任意操作都合法,但对于一条链可以发现这条链上任意操作只有一种合法方式:从最后倒着向前操作,因为只有这样能将最后的 移到最前面去。那么合法方案数是 ,其中 是每条链的长度。
先考虑处理环,考虑枚举总环长 ,那么有 种在连边时选择这么多环的方式(实际上这一步和之前枚举环的过程完全一致)。接下来只需要考虑只有链的问题,显然总共有 条这样的链。如果一条链有 个中间点,那么可以发现合法连边方式有 种,边上操作合法的概率是 。但于此同时,将中间点划分到所有链的方案数是 ,那么 相互抵消,可以发现一条 个中间点的贡献是 。最后一种确定每条链的贡献的方案数是 乘上这些 ,这里 抵消了一部分, 是合法方案数的部分, 是因为链的开头结尾可以配对。因为有标号这样不会出现问题。
后面的贡献已经和链无关,从生成函数的角度看相当于求 。那么 ln+exp 即可做到 。一种可能的思路是把生成函数看成 ,但我不会 算 。
Code
懒了,这里只有 的代码。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 10005
#define mod 998244353
char s[N],t[N];
int n,s1,s2,dp[N][N],fr[N],ifr[N],as;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int dfs(int a,int b)
{
if(a<0||b<0)return 0;
if(a+b==0)return 1;
if(dp[a][b])return dp[a][b];
int as=0;
as=(as+1ll*a*b*dfs(a-1,b))%mod;
as=(as+1ll*b*b*dfs(a,b-1))%mod;
return dp[a][b]=as;
}
int main()
{
scanf("%s%s",s+1,t+1);n=strlen(s+1);
fr[0]=ifr[0]=1;for(int i=1;i<=n;i++)fr[i]=1ll*fr[i-1]*i%mod,ifr[i]=pw(fr[i],mod-2);
for(int i=1;i<=n;i++)s1+=s[i]=='1'&&t[i]=='1',s2+=s[i]=='1';
s2-=s1;
for(int i=0;i<=s1;i++)
as=(as+1ll*fr[s1]*ifr[s1-i]%mod*fr[s2+s1]%mod*ifr[s2+s1-i]%mod*dfs(s1-i,s2))%mod;
printf("%d\n",as);
}
AGC019F Yes or No
Problem
有 道判断题,已知有 道答案是 Yes
,其它答案是 No
,但题目顺序随机。对于任意一道题目你完全无法判断它的答案。
你需要依次回答每道题,每次回答后你可以知道回答过的问题的正确答案。
求最优策略下,你回答对的问题数量的期望。答案模
Sol
首先考虑最优策略。前面的答案已知,那么可以知道当前还剩下 个 Yes
和 个 No
,但显然前面不影响后面的内部排列,后面还可以任意排列。那么可以发现下一个题答案是 Yes
的概率是 。由此容易得到最优策略: 时选 Yes
,否则选 No
,即 选 Yes
。(实际上 时也可以任意选)
然后考虑计数,直接的方式是转化为 [集训队互测2021] 生活在对角线下,然后推一页式子 解决。
但也可以更优。考虑定义当前局面的势能为 ,那么可以发现 时,最优策略是选较大的,此时势能减少当且仅当下一个选项是 对应的选项。因此此时答对当且仅当势能减少 ,这就将答对次数和势能减少量关联了起来。
但 的时候,无论如何势能不减小,而答对的概率是 。从而可以得到如下结论:
答案等于 加上过程中遇到 情况的期望次数的一半。
证明:直接归纳,分 讨论,同时贡献将两部分分开讨论。 部分之前分析过了,而期望部分显然总的期望等于当前点的期望加上随机走一步后后面的期望,当前点的期望会解决 时势能不减小的问题。
那么只需要计算后者,考虑枚举 ,那么方案数是 ,因此可以 算出这部分。
复杂度
上文提到的那个题也可以对称性做(复杂度 但是常数小),但还是被暴力 倍常数 过了。
Code
#include<cstdio>
using namespace std;
#define N 1000500
#define mod 998244353
int n,m,fr[N],ifr[N],as,li=1e6;
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int main()
{
fr[0]=1;for(int i=1;i<=li;i++)fr[i]=1ll*i*fr[i-1]%mod;
ifr[li]=pw(fr[li],mod-2);for(int i=li;i>=1;i--)ifr[i-1]=1ll*i*ifr[i]%mod;
scanf("%d%d",&n,&m);if(n>m)n^=m^=n^=m;
as=1ll*m*fr[n+m]%mod*ifr[n]%mod*ifr[m]%mod;
for(int i=1;i<=n;i++)as=(as+1ll*fr[i*2]*ifr[i]%mod*ifr[i]%mod*fr[n+m-i*2]%mod*ifr[n-i]%mod*ifr[m-i]%mod*(mod+1)/2)%mod;
as=1ll*as*ifr[n+m]%mod*fr[n]%mod*fr[m]%mod;
printf("%d\n",as);
}
AGC018E Sightseeing Plan
Problem
给定 以及
从 中选择一个点 ,类似地从 中选择 , 中选择 。
定义一种选择方式的权值为从 只向右向上走到 ,再这样走到 的方案数。求所有选择方式的权值和,答案模
Sol
从简单的情况开始。先考虑一个点走到一个矩形的方案数。相当于求 ,因为题目保证 ,可以认为这里 。考虑怎么算这个,注意到因为组合数前缀和,一行是好算的:,那么有:
感性理解:先求和行变成一列减另一列,再求和列变成四个点 ,系数分别是 。
那么点到矩形的问题可以变成四个点到点的问题,因而矩形到矩形的问题可以类似变成 个这样的问题。
但原问题需要走三步。naive 的方式是枚举中间点,然后对两侧做上面的分析把两侧变为单点问题,可以发现两侧最后得到的点和中间点无关。那么可以再将所有中间点放在一起考虑。此时变成 个如下形式的问题:
给定起点 和终点 ,对于一个矩形内的所有点,求和 走到它再走到 的方案数。
枚举点看起来没有前途,虽然直接展开也可以发现是个卷积,但是非 NTT 模数下这么多 的卷积不可能能跑。(好像有人只需要 次卷积然后卡过去了,但我不理解)
换一种形式,考虑将 到它再到 的路径拼起来。从路径的角度考虑,这相当于选出一条路径,再在上面选一个点作为中间点。可以发现一条 到 的路径的贡献是它与矩形相交的点数。
又因为中间是一个矩形,可以发现一条路径如果有贡献,那么它必定与矩形边界相交两次,一次进入一次离开。因为路径是向右向上,考虑在进入时减去当前的 ,离开时加上当前的 ,这两者的差就是中间这一段经过的点数,也即贡献。
那么问题转换为,考虑所有路径,路径从某个点进入矩形有 的贡献,从某个点离开矩形有 的贡献,显然两个可以分开算。考虑进入的部分,枚举左下边界上的每一段边,考虑左边界上 进入矩形的方案数,这显然是前后两段的组合数乘起来(此时起点终点都只有一个)。下边界则是类似的 。离开的部分类似,枚举右上边界即可。
那么可以 枚举边界,算出一个问题的答案。复杂度 ,常数不小。
Code
#include<cstdio>
using namespace std;
#define N 2000500
#define mod 1000000007
int pw(int a,int p){int as=1;while(p){if(p&1)as=1ll*as*a%mod;a=1ll*a*a%mod;p>>=1;}return as;}
int fr[N],ifr[N],s[6],t[6],as;
int C(int x,int y)
{
if(x<y||y<0)return 0;
return 1ll*fr[x]*ifr[y]%mod*ifr[x-y]%mod;
}
int solve(int sx,int sy,int lx,int ly,int rx,int ry,int tx,int ty)
{
lx-=sx;rx-=sx;tx-=sx;ly-=sy;ry-=sy;ty-=sy;
int as=0;
for(int i=lx;i<=rx;i++)as=(as+1ll*C(tx-i+ty-ry-1,tx-i)*C(ry+i,i)%mod*(i+ry))%mod;
for(int i=lx;i<=rx;i++)as=(as+1ll*C(tx-i+ty-ly,tx-i)*C(ly-1+i,i)%mod*(mod-i-ly+1))%mod;
for(int i=ly;i<=ry;i++)as=(as+1ll*C(ty-i+tx-rx-1,ty-i)*C(rx+i,i)%mod*(i+rx))%mod;
for(int i=ly;i<=ry;i++)as=(as+1ll*C(ty-i+tx-lx,ty-i)*C(lx-1+i,i)%mod*(mod-i-lx+1))%mod;
return as;
}
int main()
{
fr[0]=1;for(int i=1;i<=2e6;i++)fr[i]=1ll*fr[i-1]*i%mod;
ifr[2000000]=pw(fr[2000000],mod-2);for(int i=2e6;i>=1;i--)ifr[i-1]=1ll*ifr[i]*i%mod;
for(int i=0;i<6;i++)scanf("%d",&s[i]);s[0]--;s[5]++;
for(int i=0;i<6;i++)scanf("%d",&t[i]);t[0]--;t[5]++;
for(int f=0;f<16;f++)
{
int sx=s[0],sy=t[0],tx=s[5],ty=t[5],vl=1;
if(f&1)sx=s[1],vl=mod-vl;
if(f&2)sy=t[1],vl=mod-vl;
if(f&4)tx=s[4],vl=mod-vl;
if(f&8)ty=t[4],vl=mod-vl;
as=(as+1ll*vl*solve(sx,sy,s[2],t[2],s[3],t[3],tx,ty))%mod;
}
printf("%d\n",as);
}
AGC018F Two Trees
Problem
给两棵 个点的有根树,它们共用点集。
你需要给每个点一个权值 ,满足如下条件:
对于任意一棵有根树上的任意一个点,其子树内点权值和为 中的一个。
构造任意方案或输出无解。
Sol
Naive solution:
考虑先把每个和设为 ,此时设两棵树上分别满足条件的点权是 ,则目标是通过将一些 改为 使得 。
记 ,考虑在一棵树上将一个 改为 的影响:如果在第一棵树上这样改,相当于将 增加 并将 减少 ,那么在 上也是这样操作。而如果在第二棵树上操作,则相当于操作 ,那么是反过来的形式。
根的情况会有所不同,但可以发现根代表所有点权之和,那么如果一个根的总和 ,那么另一个根总和也需要 ,相当于 加二, 减二。
那么可以发现所有操作都相当于将某个 加二,再将某个 减二。求能否用某些给定的操作使得 。
可以发现这是一个网络流的模型:如果有奇数 则显然无解,否则考虑整体权值除以 ,变成 。此时对于一个 的操作,考虑看成一条 连向 的边,表示 将一个权值给 。然后如果 ,则从源点向 连这么多边,否则从 向汇点连相反数这么多的边。可以发现一个合法的操作方式相当于一个满流(源点汇点的边相当于让 ,中间的边相当于用操作达成这一点)。
那么考虑直接流并判断是否满流。注意到这个图是单位流量的,此时有很好的复杂度上界。首先因为流量只有 ,因此一条边最多用一次,那么一轮 dfs
的复杂度是 。从边数入手,考虑先增广 轮,这之后任意一条增广路的长度大于 (只需要证明每一轮增广后最短路增加。首先因为分层,增广后加回来的边都是从下一层连向上一层的,那么此时只存在之前留下来的从上一层到下一层的边,和一些往回走的边,因此最短路不会减少。同时如果最短路不变,那么上一轮还有可用的增广路,矛盾)。但总共只有 条单位边,因此接下来最多再有 条增广路(将它们放在一起考虑,这样不需要考虑它们中间增广将边翻回去的情况)。因此轮数不超过
因此这里这样做的复杂度是 的,可以通过。
Good Solution:
其实这个做法的思路和上一个相同,但它没有上一个那么直接。
考虑先将每个和设为 ,然后必须将每个位置改为 。
类似地定义 ,同时类似地得到若干操作 ,可以发现这些操作包含所有原先的树边,以及两条根之间的一条边。
但现在权值必须 或 ,同时初始显然 。从网络流角度考虑,可以发现这相当于每条边都必须定向,最后要求每个点入度出度相等。可以发现如下结论:
有解当且仅当每个点度数是偶数,这相当于每个点在两棵树中儿子数量奇偶性相同(一棵树上除去根外每个点向所有儿子和父亲连边,而根也向另一个根连边,因此一个点的度数是两边儿子数之和 )
证明:充分性显然:如果入度出度相等,那么原先的度数一定是偶数。必要性有很多证明方式,例如考虑欧拉回路按顺序定向,或者每次找到一组边 ,钦定它们同向并合并到 (这样不影响 的度数情况)
同时构造方案只需要构造这个定向,那么随便用上面的某种方式即可。这里欧拉回路应该较为容易实现,因为实际上不需要连成一个回路,多个环覆盖所有边也是合法的。
复杂度
Code
Sol 1:
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define N 100500
int n,va[N],vb[N],da[N],db[N],ra,rb,as[N];
int su,head[N],cnt=1,dis[N],cur[N],si;
struct edge{int t,next,v;}ed[N*10];
void adde(int f,int t,int v)
{
ed[++cnt]=(edge){t,head[f],v};head[f]=cnt;
ed[++cnt]=(edge){f,head[t],0};head[t]=cnt;
}
bool bfs(int s,int t)
{
queue<int> qu;
for(int i=1;i<=su;i++)dis[i]=-1,cur[i]=head[i];
dis[s]=0;qu.push(s);
while(!qu.empty())
{
int u=qu.front();qu.pop();
for(int i=head[u];i;i=ed[i].next)if(dis[ed[i].t]==-1&&ed[i].v)
{
dis[ed[i].t]=dis[u]+1;qu.push(ed[i].t);
if(ed[i].t==t)return 1;
}
}
return 0;
}
int dfs(int u,int t,int f)
{
if(u==t||!f)return f;
int as=0,tp;
for(int &i=cur[u];i;i=ed[i].next)if(dis[ed[i].t]==dis[u]+1&&(tp=dfs(ed[i].t,t,min(f,ed[i].v))))
{
as+=tp;f-=tp;
ed[i].v-=tp;ed[i^1].v+=tp;
if(!f)return as;
}
return as;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&va[i]);
if(va[i]==-1)ra=i;
else adde(va[i],i,1),da[va[i]]++;
}
for(int i=1;i<=n;i++)
{
scanf("%d",&vb[i]);
if(vb[i]==-1)rb=i;
else adde(i,vb[i],1),db[vb[i]]++;
}
for(int i=1;i<=n;i++)
{
int tp=da[i]-db[i];
if(tp%2){printf("IMPOSSIBLE\n");return 0;}
if(tp>0)adde(n+1,i,tp/2),si+=tp/2;else adde(i,n+2,-tp/2);
}
adde(rb,ra,1);
su=n+2;
while(bfs(su-1,su))si-=dfs(su-1,su,1e9);
if(si){printf("IMPOSSIBLE\n");return 0;}
printf("POSSIBLE\n");
for(int i=1;i<=n;i++)as[i]=da[i]-1;
for(int i=2;i<n*2;i+=2)if(!ed[i].v)
{
int f=ed[i^1].t,t=ed[i].t;
as[f]-=2;as[t]+=2;
}
if(ed[cnt].v)as[ra]+=2;
for(int i=1;i<=n;i++)printf("%d ",as[i]);
}
Sol 2:
#include<cstdio>
using namespace std;
#define N 100500
int n,a,r1,r2,as[N],is[N*2],ct,d[N],head[N],cnt;
struct edge{int t,next,id,ty;}ed[N*4];
void adde(int f,int t,int ty)
{
ct++;d[f]++;d[t]++;
ed[++cnt]=(edge){t,head[f],ct,ty};head[f]=cnt;
ed[++cnt]=(edge){f,head[t],ct,ty};head[t]=cnt;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
if(a==-1)r1=i;else adde(i,a,1);
}
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
if(a==-1)r2=i;else adde(i,a,0);
}
adde(r1,r2,2);
for(int i=1;i<=n;i++)if(d[i]&1){printf("IMPOSSIBLE\n");return 0;}
for(int i=1;i<=n;i++)
{
int u=i;
while(head[u])
for(int &j=head[u];j;j=ed[j].next)if(!is[ed[j].id])
{
is[ed[j].id]=1;
if(ed[j].ty==1)as[ed[j].t]++,as[u]--;
if(ed[j].ty==2)as[r1]+=u==r1?-1:1;
u=ed[j].t;break;
}
}
printf("POSSIBLE\n");
for(int i=1;i<=n;i++)printf("%d ",as[i]);
}
AGC017C Snuke and Spells
Problem
定义一次对整数序列的操作为:记当前序列长度为 ,删去序列中的所有 。
定义一个序列是好的,当且仅当它可以通过若干次操作删空。
定义一个序列的权值为使序列变得合法所需要的最少单点修改次数。
给一个长度为 的正整数序列, 次单点修改,每次修改后求出当前序列权值。
Sol
考虑一个操作的形式,可以发现如果有 个 ,它们对序列长度的影响一定形如:如果当前序列长度为 ,则可以将长度变为 。那么这可以看成若干条有向边,合法当且仅当可以将序列长度从 变成 。另一方面总共只有 个数,且每种数都要被操作,那么合法当且仅当所有这样的边连成一条链,或者所有边正好覆盖 (因为总长是 )
然后考虑怎么算权值。考虑在上面的过程中做出一些改编。一个直接的发现是因为可以将一些数拿走,那么从 开始不一定要跳到 ,而是可以到 中的任意位置。同时还可能将别的数变成 让它能跳到更远,但仔细分析可以发现这没有必要:如果加了 个 跳到 ,那不如将它们换为 个 ,如果有重合也不会变差。那么需要把数改过来的情况只有当前无法继续向下跳的情况,此时可以发现拿一个数过来一定更优,因为还可以继续拿一个数。考虑至少需要多少次这样的操作,即最小化不通过 跳的步数。那么因为能跳到 时也能跳到 ,可以发现找尽量往前跳的位置是最优的,那么容易发现如下结论:
考虑对于每种数 覆盖 上的所有边,其中 是 出现的次数。那么权值是 中没有被覆盖的边 的数量。
证明:首先证明权值不大于这个值。考虑之前跳的过程。按照 从大到小考虑每条 ,如果下一条线段 被当前线段包含,那么忽略下一条线段(把这些数全部拿去之后补空位);如果下一条线段不被当前线段包含,那么考虑当前线段跳到 (即只保留 个 ),然后从 开始继续向下考虑。这样可以发现每个被覆盖的位置正好用了一次,那么只需要将之前没用上的元素全部拿回来补空位即可。
再考虑证明下界。可以发现一次修改一定将一个区间长度减少 再将一个区间长度增加 ,那么它最多覆盖一个位置。
因此只需要维护有多少位置没有覆盖。根据刚才的证明,每次修改只会将两个区间的长度分别改变 ,那么直接维护每一段被覆盖了多少次然后暴力即可。
复杂度
Code
#include<cstdio>
using namespace std;
#define N 200500
int n,q,a,b,v[N],ci[N],si[N],as;
void modify1(int x,int v)
{
if(x<=0)return;
as-=!si[x];si[x]+=v;as+=!si[x];
}
int main()
{
scanf("%d%d",&n,&q);as=n;
for(int i=1;i<=n;i++)scanf("%d",&v[i]),modify1(v[i]-ci[v[i]],1),ci[v[i]]++;
while(q--)
{
scanf("%d%d",&a,&b);
ci[v[a]]--,modify1(v[a]-ci[v[a]],-1);
v[a]=b;
modify1(v[a]-ci[v[a]],1),ci[v[a]]++;
printf("%d\n",as);
}
}
AGC017E Jigsaw
Problem
有 块二维拼图,每块拼图由三部分组成:中间部分高度为 ,其两侧分别挂了一个长方形,左侧长方形的高度区间为 ,右侧长方形的高度区间为 ,保证区间不是单点,拼图不能旋转或翻转。保证所有高度区间在 内。
你需要将所有拼图放在平面上,且每一块拼图都要立在 上。相邻两块拼图可以拼接在一起,也可以分离(即拼接关系构成若干有向链),满足如下限制:
- 如果一个拼图左侧没有拼接,那么它的左侧高度区间 满足 。
- 如果一个拼图右侧没有拼接,那么它的右侧高度区间 满足 。
- 如果两个相邻拼图拼接,记左侧拼图的右侧区间为 ,右侧拼图的左侧区间为 ,则这两个区间正好在端点相交,且存在一个区间 。
求是否有解。
Sol
首先考虑判断能否拼接。考虑交点,如果交点为 ,那么情况一定是一个区间 ,另一个区间 。那么考虑使用如下方式给区间 标号:
如果 ,则标号为 ,否则标号为 。
则显然有如下结论:两个拼图能够拼接当且仅当左侧右区间和右侧左区间的标号和为 。(和为 的情况一定是 )
同时左右不拼接的条件是对应区间 ,那么问题可以看成如下形式:
有若干个标号的二元组 。你需要将它们排成若干列,使得每一列相邻两个二元组满足 且开头 ,结尾 。求是否有解。
但 还是不太好处理。考虑将所有 乘 ,那么限制变成 ,同时结尾变成 。此时可以发现 的限制相当于每一列是一条有向路径:考虑对于每个二元组连边 。则相当于找出若干条路径覆盖每条边正好一次,且每条路径起点 终点 。
那么只需要考虑后面这个问题如何解决。首先这是一个路径的问题,那么把边看成无向的,显然此时的不同连通块间没有影响,那么可以对每个连通块分开判断是否合法。
那么只用考虑弱连通的情况,按照经典方式尝试找充分必要条件:显然的必要条件是每个 的点出度不小于入度,每个 的点入度不大于出度(考虑每条从 开始,到 结束的路径的贡献,它会让一个 的点出度减入度的值加一, 部分类似)。同时如果一个连通块有边,那就至少需要一条路径(不能单独成环),因此至少需要一个点出度不等于入度。
另一方面,考虑从 的每个点连出(出度减去入度)条路径,每个 的点接收(入度减去出度)条路径。考虑 dfs
依次决定每条路径,可以发现一定不存在连不出路径的情况,否则这和度数矛盾(考虑第一条路径的情况,不能继续走下去时停在的点一定满足入度大于出度(起点需要特殊分析,但可以发现不行),那么此时可以直接在这个点结束路径,然后在剩余部分继续做即可,这一步实际上不需要弱连通性)。此时可能还剩余一些边,但现在每个点入度等于出度(差值在之前都被路径处理了),那么它们一定可以构成若干个环。因为图弱连通,那么每个环一定经过某个路径经过的点,那么将环合并到链上即可。但这里需要之前至少有一条路径才能合并上去,否则单个回路不可能拆出开头。(分析到这里也容易发现为什么需要第二个限制)
那么只需要记录每个点的入度出度,同时记录连通性,并查集或者最后 dfs
即可。
复杂度 或者一些类似的东西。
Code
#include<cstdio>
using namespace std;
#define N 100500
int n,m,a,b,c,d;
int fa[N],ind[N],outd[N],is[N],fg=1;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m*2;i++)fa[i]=i;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d",&a,&b,&c,&d);
int fi=c?c:a+m,ti=d?d+m:b;
fa[finds(fi)]=finds(ti),ind[ti]++,outd[fi]++;
}
for(int i=1;i<=m;i++)if(ind[i]<outd[i])fg=0;
for(int i=1;i<=m;i++)if(ind[i+m]>outd[i+m])fg=0;
for(int i=1;i<=m*2;i++)if(ind[i]+outd[i])is[finds(i)]=1;
for(int i=1;i<=m*2;i++)if(ind[i]!=outd[i])is[finds(i)]=2;
for(int i=1;i<=m*2;i++)if(is[i]==1)fg=0;
printf("%s\n",fg?"YES":"NO");
}
AGC017F ZigZag
Problem
有一个 行的三角形网格:第 行有 个位置,一行第 个位置和下一行的第 个位置相邻。
考虑从第一行唯一的位置开始向下(左下或右下,每一步走到下一行两个相邻位置中的一个)走到最后一行的路径,你需要选出 条这样的路径,满足如下限制:
- 第 条路径在第 条路径右侧,即对于任意一行,第 条路径在这一行经过的位置不小于第 条路径在这一行经过的位置。
- 个给定限制:每个限制形如要求第 条路径在第 步必须向左或者向右。
求方案数。
形式化地,求有多少个 的正整数矩阵 满足如下限制:
- 个给定限制。每个限制形如 ,表示要求
答案模
Sol
限制只和相邻位置有关,那么可以选一个行或列的顺序做 dp。一行上路径状态有 种,而一条路径的状态只有 种:考虑每个位置向左还是向右可以得到一个 01
串,或者考虑 这一列的差分也可以得到。
考虑对着路径 dp,设 表示顺序考虑到第 条路径,当前路径的差分表示为 的方案数。可以先只考虑相邻两条路径的限制转移到 ,然后处理不符合给定限制的状态(每个限制只关系到一个路径,可以处理出每个路径的限制然后位运算解决)
那么考虑转移,即 转移到 当且仅当 在 左侧,或者说 的前缀和的每一位对应不大于 前缀和的对应位。只需要找到一种快速的转移方式,使得每种转移正好被转移一次。
一种思路是从前缀和角度考虑, 能转移到 当且仅当前缀和对应不大于,这相当于可以通过若干次增加 单点前缀和的方式使 变成 。可以发现这样的操作只有两种:将一个相邻的 01
变为 10
或者将结尾的 0
变成 1
(其它情况不满足 01
序列的限制)。那么可以发现如下结论:
可以转移到 当且仅当可以通过若干次这样的操作将 变为 。
证明:考虑每次选一个能操作且操作后前缀和不会超过 的位置操作。然后考虑找到第一个 的位置,这个位置一定是 。接着向后找到 的下一个 1
,将这个 1
向前放(如果不存在就将最后一个 0
改成 1
)。首先这样操作不影响之后的前缀和,然后在前面部分因为这个 中间这一段 的前缀和一定严格小于 的前缀和,因此这样操作后仍然满足前缀和的限制。那么一直操作直到 即可。
但这样仍然会算重,考虑如何去重。操作相当于把 1
向前移,但不能交换两个 1
,因此操作前后 1
的顺序不变。之前的 1
可能会挡住后面的 1
,但反过来不会。考虑如下转移顺序:先将第一个 1
向左移动到需要的位置,然后移动第二个 1
,以此类推(最后可以在末尾加一个 1
然后向前移动)。即如下限制:
操作一个 1
后,之前的 1
就不能再向前了。
那么根据之前的顺序,这样的转移不会算重。同时这个转移方式就是之前证明中构造的方式,那么这样能考虑所有的转移。
然后考虑实现这个顺序的转移。一种方式是逐位考虑:如果当前是 0
则可以将后面的第一个 1
拿过来(如果没有则造一个 1
),也可以保留这个 0
(即操作不再将 1
推到前面去);如果是 1
则只能保留当前 1
。那么状态只需要额外记录当前考虑到哪一位,状态数 ,转移可以位运算做到 。
另一种方式是直接顺序考虑 1
的移动:考虑记录一个指针表示当前左侧部分的 1
已经固定了,然后进行如下操作:
- 如果指针右侧是
0
,则向右。 - 如果指针右侧是
1
且左侧是0
,那么可以交换改为10
,同时指针向左一位。如果右侧是1
也可以向右结束这一位的操作。 - 类似的,如果指针到了末尾且左侧是
0
,那么可以将左侧改为1
同时指针向左一位。
从字符串字典序(二进制表示)的角度容易发现这个转移是有顺序的,那么直接 dp 额外记录指针位置即可。状态数 ,转移直接 。
复杂度
Code
#include<cstdio>
#include<cstring>
using namespace std;
#define N 21
#define M 525000
#define mod 1000000007
int n,m,k,a,b,c,as,f0[N],f1[N],dp[M],vl[M][N],lg[M];
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<1<<n-1;i++)lg[i]=lg[i>>1]+1;
while(k--)
{
scanf("%d%d%d",&a,&b,&c);
if(c==0)f0[a]|=1<<b-1;
else f1[a]|=1<<b-1;
}
dp[0]=1;
for(int i=1;i<=m;i++)
{
memset(vl,0,sizeof(vl));
for(int j=0;j<1<<n-1;j++)
{
int tp=lg[((1<<n-1)-1)^j];
if(tp)dp[j|(1<<tp-1)]=(dp[j|(1<<tp-1)]+dp[j])%mod;
vl[j][0]=dp[j];
}
for(int j=((1<<n-1)-1);j>=0;j--)
for(int k=0;k<n-1;k++)
{
vl[j][k+1]=(vl[j][k+1]+vl[j][k])%mod;
if(((j>>k)&3)==2)vl[j-(1<<k)][k?k-1:k]=(vl[j-(1<<k)][k?k-1:k]+vl[j][k])%mod;
}
for(int j=0;j<1<<n-1;j++)dp[j]=vl[j][n-1];
for(int j=0;j<1<<n-1;j++)if((j&f1[i])<f1[i]||((~j)&f0[i])<f0[i])dp[j]=0;
}
for(int j=0;j<1<<n-1;j++)as=(as+dp[j])%mod;
printf("%d\n",as);
}
AGC016D XOR Replace
Problem
给一个长度为 的序列 ,你可以进行如下操作任意次:
记当前序列异或和为 ,将序列某一个位置替换为 。
给定目标序列 ,求将 变为 需要的最少操作次数,或输出无解。
Sol
考虑操作对 的影响。可以发现如果令 ,则新的 等于除去 外所有元素异或和再异或所有元素异或和,那么这正好等于 。因此可以发现:
考虑将 也放入序列,记 ,则一次操作一定形如交换某个 和 。
将 都转为这一形式,则操作变为交换 ,求将一个序列变为另一个序列需要的最少步数。
首先考虑是否有解,一方面如果序列元素构成的可重集不同,则显然不可能用交换达到目标。另一方面 就可以交换任意元素,那么有解当且仅当序列元素的可重集相同。
然后考虑操作次数的问题。首先考虑操作的形式,可以发现依次操作 相当于将 放到 位置, 放到 位置,以此类推,最后将 放到 位置。但如果操作相同的数这样分析就会出现问题。可以发现操作 相当于 的换位(如果中间没有其它重复元素)。再考虑处理任意操作序列。找到第一个在之前操作过的位置,将中间这一段拿出来,显然它和之前的操作不影响(不然有更早的重复出现),那么可以把这部分操作拿出来放到最前面。重复这一操作直到剩余部分没有重复出现的元素,从而操作可以变成如下形式:
- 首先进行若干次操作,每次选出一些不重复的位置 ,将它们的权值做一个轮换 ,代价是 。
- 最后选出一条链 ,然后做包含 的轮换 ,代价为 。
先考虑只有第一种操作的情况。如果序列是排列,即要求的置换唯一确定,那么一个显然的策略是对每个非单点环环做一次,代价是 。同时可以发现这是最优的:考虑证明任意操作后 (只考虑非单点环)减少量不会大于这次操作的代价。一次操作相当于将涉及到的若干个环切开,再将这些链拼成若干个环。设这次操作了 个点,涉及到了 个环。 考虑操作后这部分剩下的环。注意到代价只受单点环数量和其它的环数量影响。首先考虑单点环,这可能由之前拆出一个单点链拼出来,但被接到其它环上的部分显然不能构成单点环,那么最多有 个单点环。先考虑 的情况,此时剩余部分至少剩下一个环,那么代价最多减少了 ,而这小于 。如果 ,则代价最多减少 。
然后考虑可以有重复元素的情况。根据之前的分析需要找到一个置换 使得 ,同时最小化所有非单点环长度加一的和。首先可以发现 的点都可以直接从环中拿出来(将两侧接上不改变合法性)变成单点环,这显然更优。然后只剩下 的点,那么此时不可能有单点环,因此只需要最小化总环数。可以发现如果两个位置 相同,那么可以交换两个位置的 从而合并两个环。进一步可以发现:
考虑对于每个位置,连边 ,则最小环数等于当前有边的连通块数。
证明:首先一个环这样建边得到的还是一个环,那么它最多贡献一个连通块。另一方面对于每个连通块,每个点入度出度一定相等,那么每个连通块构造欧拉回路即可达到这个下界。
最后考虑原问题。这相当于只有最后一次操作能包含 且必须包含 ,然后代价减一。但之前的结论表明没有这个限制时最优操作是一些不相交的环,那么只需要要求这些环包含 ,然后将包含 的环放到最后即可。
那么限制变为 必须出现在环中。如果 那么它一定出现,直接用之前的过程即可。否则问题是之前的操作会删除所有自环,但因为这个点必须被操作一次,它作为单点环也不能没有代价,那它和其它环是一样的。那么删掉自环后再把这个自环加回来,然后用之前的结论即可。
复杂度
Code
#include<cstdio>
#include<algorithm>
#include<map>
using namespace std;
#define N 200500
int n,a[N],b[N],as,ct,fa[N],is[N];
map<int,int> s1;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),a[0]^=a[i];
for(int i=1;i<=n;i++)scanf("%d",&b[i]),b[0]^=b[i];
for(int i=0;i<=n;i++)
{
if(!s1[a[i]])s1[a[i]]=++ct;a[i]=s1[a[i]];
if(!s1[b[i]])s1[b[i]]=++ct;b[i]=s1[b[i]];
}
for(int i=1;i<=ct;i++)fa[i]=i;
for(int i=0;i<=n;i++)if(a[i]!=b[i])fa[finds(b[i])]=finds(a[i]),is[finds(a[i])]=1,as++;
for(int i=1;i<=ct;i++)if(is[i]&&fa[i]==i)as++;
if(a[0]!=b[0])as-=2;else if(is[finds(a[0])])as--;
sort(a,a+n+1);sort(b,b+n+1);
for(int i=0;i<=n;i++)if(a[i]!=b[i])as=-1;
printf("%d\n",as);
}
AGC016E Poor Turkeys
Problem
有 个数 。依次进行 次操作,每次操作为如下形式:
给定 ,如果 都不存在则不操作;如果只有一个存在则删去这个数;如果两个都存在则随机删去其中的一个。
求有多少对 满足如下性质:所有操作后 可能同时留下。
Sol
首先考虑一个更简单的问题:如何判断一个数的情况,即 是否可能留下。
之前的选择可能在很多步操作后传递到其它点上(例如 ),可以发现正着做是困难的。考虑倒过来,先钦定 留到最后,看是否会产生矛盾。如果 留到最后,那么每一次涉及到 的操作 一定是如下情况: 在这次操作前都存在,这次操作删掉了 。那么对于每一个和 操作过的数,这样确定了它被删掉的时刻,一个数被确定两个不同的时刻就显然不合法了。再对这些数考虑,可以发现如果确定了 在 时刻被删除,那么 在 时刻前也不能被删。那么和之前的讨论类似, 时刻前和 操作过的数必须在这一次操作被删掉。那么考虑每确定一个数就向前做这样的判断并继续确定数,直到不会再扩展或者发现矛盾。单次复杂度 但也可以做到 (注意到每个点只能被确定一次,那么出现不合法状态前有效的判定只有 次)
然后考虑两个点,直接的做法是先钦定 都留到最后,然后和之前一样做。但如果单次做到 ,这样复杂度就变成 了,然后就过了。
但还可以稍微优一点。考虑判定是否合法,naive 的想法是考虑只保留单个时得到的限制(数 必须在时刻 被删掉),如果两组限制矛盾那么显然不合法。然后发现过了。仔细分析一下,如果两组限制甚至不相交那显然不会有问题,问题在于还有情况是限制相交但是限制了相同的值。但可以发现并没有这种情况:考虑一个限制是如何出现的,它一定是从 开始倒序的若干次操作 ,它限制的时刻是最后一次操作的时刻。如果从不同的 开始到同一个 ,考虑反过来 出发到 的路径上第一次分叉的位置,这个位置的限制是从不同边转移来的,那么这里得到了两个不同的限制,因此必定矛盾。那么可以发现限制相交必定矛盾,合法当且仅当限制不相交。因此 预处理出每个 能导致的限制,然后问题相当于给定 个集合,求有多少对集合没有交。那很容易做到 ,也可以显然地把问题看成矩阵乘法(将集合看成 01
向量,那么没有交当且仅当内积为 ,求两两内积相当于矩乘),那么甚至可以做到 但这个范围应该所有矩乘都跑不过bitset
这里也可以完全从时间考虑:倒推时间,记录当前时间点必须保留的位置集合 ,每当看到一个操作 时,如果 则矛盾, 则将另一个元素加进来(和刚才的讨论相同), 则不操作。但这个做法思路本质相同。
然后这里也可以正回去:记录每个点想保留到当前需要哪些数在之前被删去(以及能不能保留下来),理论上这部分复杂度 但因为剪枝跑得飞快( 的优势体现不出来)。
Code
#include<cstdio>
#include<vector>
#include<queue>
#include<bitset>
using namespace std;
#define N 405
struct sth{int nt,ti;};
int n,m,a,b,ds[N],as,fg[N];
vector<sth> ed[N];
bitset<N> si[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
ed[a].push_back((sth){b,i});
ed[b].push_back((sth){a,i});
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)ds[j]=0;
fg[i]=1;ds[i]=m+1;
queue<int> qu;qu.push(i);
while(!qu.empty()&&fg[i])
{
int u=qu.front();qu.pop();
for(int j=0;j<ed[u].size()&&ed[u][j].ti<ds[u];j++)
{
if(ds[ed[u][j].nt]){fg[i]=0;break;}
ds[ed[u][j].nt]=ed[u][j].ti;qu.push(ed[u][j].nt);
}
}
for(int j=1;j<=n;j++)si[i].set(j,!!ds[j]);
}
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(fg[i]&&fg[j]&&!(si[i]&si[j]).any())as++;
printf("%d\n",as);
}
AGC016F Games on DAG
Problem
给一个 个点 条边的 DAG,保证 是它的一个拓扑序。
你可以选择保留边的一个子集,求满足如下限制的方案数,答案模 :
考虑在只保留边的 DAG 上进行如下游戏:有两枚棋子初始分别在 ,两人轮流操作,每次操作必须选择一枚棋子将其沿着一条出边移动,不能操作者输。在双方最优操作下此游戏先手必胜。
Sol
根据经典博弈结论,两枚棋子可以看成两个独立的游戏。同时考虑计算一枚棋子在每个点上的 sg 值:
然后整个局面的 sg 值等于 ,那么先手必胜当且仅当 。
(完整证明一遍 Nimber 我不大会,而且不轻松,所以放弃了)
首先考虑枚举整个图的 sg,考虑达到一种 sg 的方案数。可以发现 sg 只需要满足如下限制:
对于一个 的点,它不能连向任何 的点。且对于任意 ,它必须至少连向一个 的点。
因为边固定,枚举 sg 后容易算出合法的方案:考虑每个点和连向的每一种 即可。但 sg 的数量等价于有标号集合划分,这在 下不能接受。
考虑 dp,但之前的判定方式有一个问题:sg 大的点需要用到所有 sg 更小的点的具体值(因为需要连向每一种值),这是状态不能接受的。
考虑换一个顺序判定:从小的 sg 向大的 sg 考虑,第一个限制仍然是 sg 相同的点之间不能连边,而第二个限制变为考虑所有 的点,对于任意一个 的点其必须连向至少一个 的点。
那么考虑从小到大填,第二个限制需要考虑所有 的点,那么考虑一次填所有 的点,这样可以处理 的要求,也可以直接判断第一个限制;同时对于转化后的第二个限制只需要考虑每个 的点连向所有 的点连的边,如果一个点连过来 条边则方案数是 ,然后将每个 的点的权值乘起来就是这一步的贡献。但这样只处理了 sg 大的点连向 sg 小的点的边和相等的边,可以发现小的连向大的的边可以任意,那么填 的时候考虑数一下有多少从 到 的边,然后乘一个 。
可以发现这个过程中具体值是不重要的(判断合法也只关心是否相等),那么考虑 表示当前填了前若干种 sg,已经填了的部分构成集合 。考虑加入下一种 sg 的集合 ,根据之前的方式处理 到 的连边,其中 过去的可以任意连,而剩余部分每个点至少需要连向一个 中点( 之间不能连边),然后即可转移到 。
暴力处理系数时复杂度是 的,但说不定也能过。稍微预处理每个点连出和连入的边构成的集合,然后用位运算优化+预处理popcount,枚举每个点 算到集合的连边数就可以做到 。进一步可以发现固定 后剩余每个点贡献的乘数是独立的()。那么先枚举 算每个加入点的贡献,然后用减 lowbit 或者类似的方式求每一个子集和(乘积),即可 求出所有转移系数,复杂度做到 但我没写
Code
#include<cstdio>
using namespace std;
#define N 16
#define M 33001
#define mod 1000000007
int n,m,a,b,s[N][N],vl[N][M];
int dp[M];
int main()
{
scanf("%d%d",&n,&m);
while(m--)scanf("%d%d",&a,&b),s[a][b]=1;
for(int i=1;i<=n;i++)for(int j=1;j<1<<n;j++)
{
int v1=0;
for(int k=1;k<=n;k++)if(s[i][k]&&((j>>k-1)&1))v1++;
vl[i][j]=(1<<v1)-1;
for(int k=1;k<=n;k++)if(s[k][i]&&((j>>k-1)&1))vl[i][j]=2*vl[i][j]%mod;
}
dp[0]=1;
for(int i=0;i<1<<n;i++)if(dp[i])
for(int j=((1<<n)-1)^i;j;j=(j-1)&(((1<<n)-1)^i))if((j&3)<3)
{
int v1=dp[i];
for(int k=1;k<=n;k++)if((~(i|j)>>k-1)&1)v1=1ll*v1*vl[k][j]%mod;
dp[i|j]=(dp[i|j]+v1)%mod;
}
printf("%d\n",dp[(1<<n)-1]);
}
AGC015E Mr.Aoki Incubator
Problem
数轴上有 个人,第 个人初始在位置 (位置两两不同),他会以 的速度向正方向前进(保证 且两两不同)
每个人有一个 中的权值 ,如果两个人相遇,他们的权值会同时变为两人权值中的最大值。
求有多少种给每个人一个初始权值的方式,使得足够长时间后每个人权值都是 。答案模
Sol
首先考虑如何描述相遇的事件。一种最直观的方式是考虑将时间和坐标看作两维 。以向上为时间前进的方向,则每个人的移动可以看成 在 部分的一条射线。那么此时两人相遇对应两条射线的一个交点,且所有相遇事件按照交点的 顺序发生。
相遇时一个 可以传导到另一个,这相当于 的传导可以在这里转向。因此可以发现如下结论:
在确定了一组初始权值后,一个人最后为 当且仅当存在一个初始为 的人开始往 增加的方向走,遇到交点可以转向的情况下可以走到这个人所在的直线。
证明:转向相当于相遇时一个 传给另一个位置的情况,转向可以和传导对应。
那么考虑求出一个初始位置最后能走到哪些人,一种方案合法当且仅当选出的值为 的位置最后能覆盖所有人。
但如果一个位置能到的人集合是任意集合那显然不能做,因此首先考虑一个位置能走到的人的性质。类似 028F 地,从几何直观角度容易发现:
将所有人按照最终的位置(即按照 顺序,因为 分别不同没有奇怪情况)排序,则每个人可以走到的人构成一个区间。
证明:记当前人为 ,考虑 能到的最左侧的人 和最右侧的人 。考虑 从 分别最后走到 的路径,此时对于 中间的一个人,他自身有一条从 过来的路径,如果初始他位置 的位置则这条线显然和 的路径相交,否则类似地和 的路径相交。而如果存在路径相交,则一定可以在交点转向从而走到这个人的直线。
那么再考虑求出区间。先考虑求出能到的 最大的人,另一个方向显然对称。
naive 地想,只考虑传递一次,那么 的最大值至少是满足 (这里 指当前考虑的人的初始位置)的人中 的最大值。但还有多次传递到达一个更大的 的情况。考虑这种情况是否会出现。因为只考虑 的最大值,所以这里只需要考虑最后到达的人的 比传递中途经过的所有 都大的情况(否则可以在中间就停止得到更大的 )。此时考虑最后一个人的参考系,那么传递过程中的人都在向左走,那么如果一个人出发可以传递到最后一个人,那他初始必须在最后一个人右侧(否则他不可能向右传递);但此时第一个人就可以直接传给最后一个人。那么可以发现如下结论:
在考虑最大最小的 时,不需要考虑多次传递的情况。因而 的最大值就是满足 位置中 的最大值,最小值类似对称。
这样就可以 求出每个位置最后能覆盖的区间,那么问题变为如下形式:
给 个区间 ,求有多少种选出若干区间覆盖所有 个点的方案。
直接做有一点麻烦(虽然也能做),但可以发现这个问题还有一些很好的性质:注意到右边界是前缀最大值,左边界是后缀最小值,那么显然有 单调不降,这样就避免了区间严格包含带来的转移顺序问题。
具体来说,在 单调不降的情况下,判断 个区间 能否覆盖整个线段 只需要将区间按照下标排序,然后判断如下条件:。证明是显然的。那么直接按照下标顺序转移即可。从下标考虑相当于每个 可以从前面一段区间转移过来,同时因为 不降这个区间端点显然也是不降的,那么双指针记录能转移的区间然后扫过去即可;也可以用下标做指针。
但因为要排序,复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 200500
#define mod 1000000007
#define ll long long
int n,x[N],y[N],f[N],g[N],p[N];
int lb[N],rb[N],dp[N];
ll tp[N];
void doit(int *x,int *f)
{
for(int i=1;i<=n;i++)tp[i]=1ll*x[i]*(n+1)+i;
sort(tp+1,tp+n+1);
for(int i=1;i<=n;i++)f[tp[i]%(n+1)]=i;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]);
doit(x,f);doit(y,g);
for(int i=1;i<=n;i++)p[f[i]]=g[i];
for(int i=1;i<=n;i++)rb[i]=max(rb[i-1],p[i]);
for(int i=n;i>=1;i--)lb[i]=min(lb[i+1],p[i]);
int su=1,li=1;dp[0]=1;
for(int i=1;i<=n;i++)
{
while(li<lb[i])su=(su+mod-dp[li-1])%mod,li++;
dp[rb[i]]=(dp[rb[i]]+su)%mod;su=2ll*su%mod;
}
printf("%d\n",dp[n]);
}
AGC015F Kenus the Ancient Greek
Problem
定义正整数对 的欧几里得步数 为:
- 如果 ,。
- 如果 ,。
- 否则,。
给定 ,求所有满足 的 中 的最大值,并求出达到最大值的 对数(答案模 )。
组数据。
Sol
首先分析 相关的性质。不妨设 (否则先交换一次),那显然整个 Euclid 的过程是取模,交换,取模,交换再重复直到取模后得到 。
只看 不太好分析整个过程,考虑进一步刻画这个过程。可以发现只需要记录每次取模的 ,即减掉的倍数,就可以描述整个取模过程;再加上最后的 ,就可以还原结果:考虑记每次取模的结果为 ,那么从 开始倒推,这次取模结果为 时可以从 倒推到 。因此考虑将取模结果倒过来记录得到序列 ,再记录 ,然后从 倒推即可还原 。(事实上反过来之前这个 序列等价于连分数展开)显然 和 形成一一对应。
考虑 序列需要满足的限制,首先显然 。如果后面有余数(),那操作后一定有 ,那么前面取模就不是问题。但在最后一步时,如果 前面就得到了 ,但这不是取模能模出来的东西,只在第一步可以出现。因此限制可以看成序列长度为 或者 ,但(从下一步可以发现)这个限制很麻烦。可以发现不考虑取模, 的操作结果等价于 的操作结果(证明显然)。那么考虑转而限制第一个位置必须是 ,然后操作次数变为序列长度减一。但这样还是会在 上出问题,因此还是需要特判最大值为 的情况。
先考虑最大值,相当于求出使得最后 不超过某个上限的合法序列的最大长度。那显然 应该取 (因为相当于后面的结果再乘 )。再考虑 的部分,因为是 ,容易发现 全部取 是最优的(这样开头必须是 的限制就被解决了)。证明也很显然:考虑任意一个其它方案,只需要证明每一轮操作后得到的 与全 得到的 满足 ;那么只需要在这个条件加上 的情况下证明 (同时 )。
考虑所有 的过程,那么每一步是从 推到 ,而这显然是斐波那契数。因此记 ,由刚才的分析可以得到如下结论:
在 时, 满足 同时偏序所有 且 的数对 。因而最大值大于等于 当且仅当 排序为 后 。(这样考虑不到 ,因此需要特判 )
这样就解决了最大值的问题,枚举 即可。然后考虑计数,相当于统计有多少 和给定长度的 满足最后生成的 在上界内(可能还需要考虑交换 的情况,但 时也不存在 的最优解)。
首先考虑 , 的情况可以看成将上界全部除以 ,然后做 的问题。但可以发现上界除以 就很影响答案了:任何情况下 (只需要 ),因此两个上界同时乘以 后答案至少 (下一个一定放得下),从而除以 一定减少答案。因此可以不考虑 的情况。(这仍然只对 成立,事实上 时答案是 , 也可行)
然后考虑 。不妨设 ,因为 是最优的,所以现在一定有 或者 。如果是前者那情况看起来比较正常(因为最大的数都被限制了),但后者只限制了整个过程中第二大的数 而没有限制 的范围,此时最后一步 可以取得很大(例如 的情况, 是一组答案为 的最优方案)因此方案数可能很大。但倒数第二步时还有 ,因此这种情况的倒数第二步和上一种情况的最后一步都相当于如下问题:
求出所有长度为 且使得最后 的序列 (如果取全 最后结果是 )
考虑此时有多少方案。可以发现如下结果:
- 如果某一步 ,则最后一定有 。
证明:考虑接下来的两步操作,本来是 ,但此时变为 。可以发现最后两个元素至少是之前的 倍(因为 ),那么 时这至少就是 ,但 是趋向于 的(带入通项容易发现。从连分数角度, 无限下去会得到 ),且在 之后都小于这个值(算一下不难发现),那么乘 显然超过了这个界。
- 如果有两个 ,则最后一定有 。
证明:如果两个 分开,那么每个至少乘上 ,而这东西的平方 显然超过 。如果两个 相邻,手算一下 和 ,这样的最后两项至少是之前的 倍(同样用到了 )。
那么最终可以得到如下结论:
在这一限制下,序列只可能全 或者中间加入一个 。因此可能的序列只有 个。
用这一结论还可以将之前的两种情况几乎合二为一:
- 如果 且序列长度为 ,那么 。
证明:对 归纳。如果 ,那这一步只能是 (考虑 )。因而上一个元素 ,归纳可得上上个元素 。但 ,因此前一步只能 。根据之前结论,为了 这里只能 且之前全部 。但可以发现此时的序列为 ,那么还是做不到大于。
(这也说明取等是可行的,例如 )
而这个等号不影响之前的分析(因为直接倍数更大),此时可能的序列还是只有 个。那么考虑枚举到倒数第二步时的情况。对于每一个当前可能的情况 ,枚举 的顺序,然后相当于求有多少个 满足 ,解方程即可。
最后需要将之前特判的答案为 的情况补上,但此时显然任何方案答案都是 。
事实上合法序列也可以直接搜:考虑依次填数,如果后面全 还不合法就停止,可以发现在 上再操作几次 最后得到的最大数形如 ,可以快速判断。(序列可以预处理,因此不这样优化也能过)
可以不需要更慢的预处理,复杂度 。但预处理带个 也能过。
Code
#include<cstdio>
#include<vector>
using namespace std;
#define N 105
#define ll long long
#define mod 1000000007
int T;
struct sth{ll a,b;};
ll fib[N],a,b;
vector<sth> si[N];
void dfs(sth sr,int n,int d)
{
if(n==d){si[d].push_back(sr);return;}
int vl=1;
while(sr.a||vl<2)
{
sth s2=(sth){sr.b,sr.a+sr.b*vl};
int tp=d-n;
if(s2.b*fib[tp]+s2.a*fib[tp-1]>fib[d+2])break;
dfs(s2,n+1,d);vl++;
}
}
int main()
{
fib[0]=fib[1]=1;
int nw=1;
while(1)
{
fib[nw+1]=fib[nw]+fib[nw-1];nw++;
if(fib[nw]>2e18)break;
}
for(int d=0;d<=nw-2;d++)dfs((sth){0,1},0,d);
scanf("%d",&T);
while(T--)
{
scanf("%lld%lld",&a,&b);
int as=1,ct=0;
for(int t=0;t<2;t++)
{
int nw=0;while(fib[nw+1]<=a&&fib[nw+2]<=b)nw++;
if(as>nw)continue;
if(as<nw)as=nw,ct=0;
for(int i=0;i<si[nw].size();i++)if(si[nw][i].b<=a&&si[nw][i].a<=b)
ct=(ct+(b-si[nw][i].a)/si[nw][i].b)%mod;
a^=b^=a^=b;
}
if(as==1)ct=a*b%mod;
printf("%d %d\n",as,ct);
}
}
AGC014E Blue and Red Tree
Problem
有一棵 个点的树 (有标号),初始所有边都是蓝色。你可以进行如下操作:
选择 满足 且 到 的路径上都是蓝色边。然后删去 到 路径上的一条边,加入一条 的红边。
给定目标树 ,你需要通过 次操作使得 且 全部变成红边,求是否存在这样的方案。
Sol
首先可以发现操作只涉及到蓝边,那么之前加入的红边不影响之后的操作。那么操作形式可以看成选一条还没有操作过的红边,满足其两端点还被蓝边连通,然后删去路径上一条蓝边。合法当且仅当能执行 次操作。
正着考虑,一次操作会影响后面操作的合法性所以看起来有点困难,但操作限制是两端点被蓝边连通,那么如果一个红边的操作当前不能做了,那之后也不能做,从而一定不合法。因此每一步操作必须要求不能使之后的操作不连通。但因为蓝边是一棵树,两点连通当且仅当两点树上路径之间的边全部都没有被删掉。那么可以发现如下结果:
考虑每条红边在 上覆盖两个端点间的路径,那么能操作一条红边删去一条连蓝边当且仅当在还没有操作的红边中只有这条红边覆盖了删去的蓝边。
证明:首先为了完成操作这条红边必须覆盖删去的蓝边,而如果还有更多的覆盖,那些红边接下来就不连通了。
同时如果一条蓝边只剩一次覆盖了,那只能是这条红边和它操作,不存在别的可能。刚才的分析说明这样操作不影响之后的操作,因此合法操作一定形如:
使用之前的方式覆盖,选出一条只被覆盖了一次的蓝边,删去这条蓝边和覆盖它的红边(同时去掉覆盖)
那么问题相当于链上 ,每次操作后找出所有覆盖次数变为 的边然后对这些边继续操作。如果没法操作了或者有边权值变为了 就不合法。直接树剖复杂度即为 ,也可以做到
也可以倒过来考虑,此时操作相当于加一条边使得红边两端点被这条边连通且红边两端点的路径经过加入的边。可以发现这相当于选出一条两端点不在同一蓝边连通块中的红边,然后在这两个蓝边连通块间加一条边并合并连通块。因为加蓝边必须是树上合并,任意两个蓝边连通块间最多有一条可以加入的蓝边,且无论之后怎么合并还是这条蓝边。那么可以发现如下结果:
如果一条红边两个端点所在的蓝边连通块间有一条 中还没连上的边,那么这条红边一定将这条蓝边加进来。
证明:无论之后怎么合并,这条红边一定在这两个连通块间,且这两个连通块间只会有一条蓝边。那么红边只能操作这条蓝边。
同时可以发现合并不影响其它能做的操作(唯一例外是两条红边连接同样两个蓝边连通块,但这两个蓝边连通块只能有一条蓝边,因此这样一定不合法),那么倒着做也可以直接确定顺序:
找到任意一对连接相同连通块的蓝边和红边,删去这两条边并合并连通块。
考虑启发式合并(合并时较小的一侧改为较大的一侧)维护每条边两侧的连通块编号,那么只会改 次编号。然后用 map
或者 set
或者类似的东西不断找相同的边即可。复杂度 。
Code
只写了第二种.jpg
#include<cstdio>
#include<algorithm>
#include<set>
#include<queue>
using namespace std;
#define N 105000
int n,a,b,sz[N],fa[N],ci;
set<int> s[2][N];
queue<pair<int,int> > qu;
int finds(int x){return fa[x]==x?x:fa[x]=finds(fa[x]);}
void del(int tp,int x,int y){s[tp][x].erase(y);s[tp][y].erase(x);}
void ins(int tp,int x,int y)
{
if(s[!tp][x].find(y)!=s[!tp][x].end()){del(!tp,x,y);qu.push(make_pair(x,y));return;}
s[tp][x].insert(y);s[tp][y].insert(x);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)fa[i]=i;
for(int t=0;t<2;t++)for(int i=1;i<n;i++)scanf("%d%d",&a,&b),ins(t,a,b),sz[a]++,sz[b]++;
while(!qu.empty())
{
pair<int,int> si=qu.front();qu.pop();
int x=finds(si.first),y=finds(si.second);
if(x==y)continue;if(sz[x]<sz[y])swap(x,y);
sz[x]+=sz[y];ci++;fa[y]=x;
for(int t=0;t<2;t++)
while(s[t][y].size())
{
int u=*s[t][y].begin();
del(t,u,y);ins(t,u,x);
}
}
printf("%s\n",ci==n-1?"YES":"NO");
}
AGC014F Strange Sorting
Problem
考虑对排列 的如下操作:
将 中所有前缀最大值拿出来得到序列 ,所有非前缀最大值拿出来得到序列 ,然后将 换为 后面拼接 的结果。
给一个 阶排列 ,显然不断执行上述操作可以将其排序,求将其排序需要多少次上述操作。
Sol
直接维护操作非常困难:前缀最大值的变化很难用更少的信息处理,但这个操作会使得前缀最大值有很大的变化。一种可行的方式是考虑将不断将排列的前缀最大值序列取出,得到若干个序列,此时删掉一个序列后的前缀最大值就是下一个序列,相当于将第一个序列删掉,然后将元素在末尾再依次加入这些序列。这样最后可以获得一个类似 naive solution 的做法,但还是没有前途。
那么考虑不从操作时序处理,换一种方式。可以发现值域也是一个有效的角度。一种想法是考虑最大值,显然一次操作后最大值永远在最后,那么可以删去这个最大值(这也证明了操作次数有限且不超过 次)。但这样还是用了操作顺序,因此不大行。另一种想法是考虑最小值。可以发现最小值有更好的性质:显然 不影响更大的位置是不是前缀最大值,那么删去 后所有操作形式不变。因此考虑先求出删去 后的答案 ,这样重新考虑 后显然需要至少 次操作将 排序。此时将 放回去, 次操作后 可能出现在 中的任一位置。可以发现此时可能不需要操作,否则一次操作一定可以排好序(此时前缀最大值可以选出 间所有数)。那么只需要再考虑判断将更大的数排好序后 是否在最前面。
但直接判断还是非常难,因此考虑在处理 间所有数的问题时维护一些信息,用这些信息解决 的问题。经过尝试可以发现这确实可行:
Naive Solution:
注意到最后只需要判断 是否在开头。考虑如果 出现在开头,接下来再经过多少次操作后 又来到开头。因为 是最小的数,因此前面有数时 不会被选到,那么前面的都被移到后面后 会再次来到开头。因为每次是删前缀最大值,考虑如下过程:
对于一个排列,每次取出当前的所有前缀最大值得到一个序列,重复这个过程直到结束。
记一个排列的权值是它经过上述操作可以被分成多少个序列,记 表示第 次操作后排列的权值。那么由定义可以发现:
如果 在第 次操作时在开头,那么它下一次出现在开头是第 次操作之前。
考虑 的改变。从操作顺序的角度,考虑一次操作对 的影响。从划分出来的序列考虑,可以发现一次操作相当于拿出第一个序列,将其所有元素放到最后。那么之前剩余的划分显然不会改变,但这些划分可能会通过最后的元素扩展:显然每次选出序列的末尾最大值单调递减(每次都可以选走最大值),那么在加上后面的元素(这部分是递增的)后,剩余的第一个序列扩展时会拿走比它之前末尾更大的所有元素,然后第二个序列扩展拿走一些元素,以此类推。最后还有可能留下来一些最小的元素(例如后面有 ),那么它们可能新划分一个序列。由此可以发现 。
然后再从加入 的角度考虑。考虑一次 在开头的操作,这次操作后 被放到了后面新加入的位置,那么后面一定会新划分一个以 开头的序列。如果 ,那么 可以加入到原本这里新加入的序列的开头,那么接下来的操作就和没有它的情况相同,直到 下一次被拿出来。而如果 ,那么加入 后 会新增加一个只有 的序列,从而 会加一。再考虑之后操作的影响,如果之后的操作不新增序列(),那么 这个序列会继续保留;而如果有一个操作新增了一个序列,那么 就可以放到这个操作的开头,再往后就不增加 了。可以发现不考虑 时,操作 一定会新增一个序列;那么在 回到开头前它一定可以合并。因此对 的影响为如下形式:
考虑每次 在开头的操作 ,如果 ,则向后找到第一个 的位置 ,将 这一点的 全部 ,否则不操作。
最后答案是第一个满足 的位置下标。
直接跳 最坏需要 次,但注意到 单调不增且跳到 的位置时没有修改,那么考虑一段相同的 快速跳过去,这样只会跳 次(一些和为 的数最多有这么多种不同的)。修改容易 BIT 维护(考虑维护差分)。复杂度 ,但是它能过(1.7s/2s)
Good Solution:
仍然考虑判定是否合法。如果考虑操作次数就会变成之前的情况,因此考虑完全不用操作时序的方式,这就需要找到一些完全不变的东西。
那么考虑不从整体出发,而考虑局部性质。首先 在最前面的限制可以改为 在 前面,那么判定这个条件就只需要考虑两个数。但这两个数的轮换还和整体有关,那么仍然无法维护。
再试一次,进一步考虑最小的三个数 。考虑一次操作对这三个数的影响。首先考虑一次前缀最大值操作能选出哪些数。可以发现取了一个 的数后就不会再取更小的数,因此每次前缀最大值在这三个数中取出的一定是只考虑这三个数的前缀最大值的一个前缀。那么大部分时候这是三个数的一个前缀,唯一的特例是 出现在开头。在这个特例出现之前,可以发现将一段前缀放到最后的操作不改变这三个数间逆序对数量的奇偶性,这时如果 以及后面的数都排好序了,就可以唯一确定 是否在 前面。
可以发现这个特例出现的情况一定是 在 排好序之前就相邻了(显然 相邻后不可能再被别的操作拆开)。而之前的分析说明如果加入 的时候会多一次操作,那么 只有在最后多出来的这次操作上才能到 的前一个位置。那么如果上一步需要多一次操作,此时就不会出现特例的情况。那么终于可以得到如下结论:
如果 的过程中需要多一次操作,那 的过程需要多一次操作当且仅当 这三个数间的逆序对数量为奇数。
考虑更一般的情况。显然上述分析不能处理特例,考虑换一个数避免特例。那么考虑向上找到最后一次需要多一个操作的位置,设这个位置是 。这相当于最后一次操作前 排好序了, 排好序了,最后一次操作选 作为前缀最大值将它们合并。且此时可以发现 必定在开头,否则不可能达成这样的操作。
那么直觉上就应该考虑 ,然后可以发现它是对的。即如下结论:
记从 推到 的过程中最后一次需要增加操作的讨论是 ,则 的过程需要多一次操作当且仅当 这三个数间的逆序对数量为奇数。
证明:只需要证明同样不会有特例出现。但此时多出的情况是选了一些 之间的数,然后再选 ,这样又变得非常奇怪。考虑证明不存在这种情况。如果存在这一情况,那么操作后存在一个 之间的数在 左侧一位。但可以发现如果 左侧是一个小于它的数,那么接下来每次操作后左侧的数都小于它(如果前缀最大值选到左侧的数,那也会选 ;而如果选了 ,那它左侧也会至少选一个小于它的数)且它也不可能在开头,那么这与最后的操作矛盾。去除这种情况后可以发现选 中数后之后不会再选 中的数作为前缀最大值。
这样就结束了,只需要在过程中记录上一次增加操作次数的 的位置。还有一些细节在于开头的处理(答案仍然是 的时候),但可以发现答案是否变成 的判定是容易的;这里也可以处理成之前的形式。
复杂度
Code
Naive:
#include<cstdio>
#include<set>
using namespace std;
#define N 200500
int n,a,rp[N],st[N],ct,v[N],li,cr;
struct BIT{
int tr[N];
void add(int x,int k){for(int i=x;i<=n;i+=i&-i)tr[i]+=k;}
int que(int x){int as=0;for(int i=x;i;i-=i&-i)as+=tr[i];return as;}
int getnxt(int x,int &ci)
{
int vl=que(x),nw=0;
ci=vl;
for(int i=17;i>=0;i--)if(nw+(1<<i)<=n&&tr[nw+(1<<i)]<=vl)nw+=1<<i,vl-=tr[nw];
return nw+1;
}
int getnxt1(int x)
{
int vl=x-que(x),nw=0;
for(int i=17;i>=0;i--)if(nw+(1<<i)<=n&&(1<<i)-tr[nw+(1<<i)]<=vl)nw+=1<<i,vl-=(1<<i)-tr[nw];
return nw+1;
}
}tr;
void doit(int x)
{
while(1)
{
int ci=0,vl=tr.getnxt(x-1,ci);
if(vl==n+1)return;
if(vl==x)
{
vl=tr.getnxt1(x);
tr.add(x,-1);tr.add(vl,1);
continue;
}
ci=li-ci;
x+=ci*((vl-x-1)/ci+1);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a),rp[n+1-a]=n+1-i;
for(int i=1;i<=n;i++)
{
int lb=1,rb=ct,as=ct+1;
while(lb<=rb)
{
int mid=(lb+rb)>>1;
if(st[mid]<rp[i])as=mid,rb=mid-1;
else lb=mid+1;
}
st[as]=rp[i];v[i]=as;ct+=as>ct;
}
for(int i=1;i<=n;i++)
if(li<v[i])
{
int nt=tr.getnxt1(0);
li++;
if(li>1)tr.add(nt,1),doit(li);
}
else doit(v[i]);
int as=n;while(as&&tr.que(as)==tr.que(as-1))as--;
printf("%d\n",as);
}
Good:
#include<cstdio>
using namespace std;
#define N 200500
int n,p[N],rp[N],as,l1,l2;
bool chk(int a,int b,int c){return ((a>b)+(b>c)+(a>c)+1)%2;}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&p[i]),rp[p[i]]=i;
l1=n+1,l2=n+2;
for(int i=n;i>=1;i--)if(chk(rp[i],l1,l2))l1=rp[i];
else as++,l2=l1,l1=rp[i];
printf("%d\n",as);
}
AGC013E Placing Squares
Problem
给定 ,考虑 个格子组成的一行,你需要将这些格子划分成若干段,满足如下 个给定的限制:
限制 :位置 不能和 分开。
定义一种划分方式的权值是每一段长度平方的乘积,求所有满足限制的划分方案的权值和。答案模
Sol
因为 很大,显然不可能真的维护长度。那么考虑换一种方式处理这个权值。一种经典的方式是维护组合意义:, 相当于在 个中选两个数, 相当于选一个。乘积相当于每一段都做这样的操作,求总方案数。那么问题变为如下形式:
你需要将格子划分为若干段,再在每一段内部选一个或两个位置,选两个位置有 的权值,求方案数。
此时可以得到一个简单的 dp:记 表示考虑到位置 ,当前 所在的段一共选了 个位置时前面的方案数。转移时枚举 是否分开,然后枚举 选不选。那么根据当前位置有没有限制有两种形式的转移。但第二种转移只有 个,因此考虑将 dp 写成矩阵,连续的一段第一种转移用矩阵快速幂处理,这样就避免了 的转移。
复杂度
Code
#include<cstdio>
using namespace std;
#define mod 1000000007
int n,k,a,ls;
struct mat{int v[3][3];mat(){for(int i=0;i<3;i++)for(int j=0;j<3;j++)v[i][j]=0;}}s0,sb,t;
mat operator *(mat a,mat b)
{
mat c;
for(int i=0;i<3;i++)for(int j=0;j<3;j++)for(int k=0;k<3;k++)
c.v[i][j]=(c.v[i][j]+1ll*a.v[i][k]*b.v[k][j])%mod;
return c;
}
mat pw(mat a,int b)
{
mat as;for(int i=0;i<3;i++)as.v[i][i]=1;
while(b)
{
if(b&1)as=as*a;
a=a*a;b>>=1;
}
return as;
}
int main()
{
s0.v[0][0]=s0.v[0][1]=s0.v[1][1]=s0.v[2][2]=1;s0.v[1][2]=2;
t.v[0][0]=t.v[1][1]=t.v[2][2]=1;
sb=t;sb.v[2][0]=sb.v[1][0]=1;
scanf("%d%d",&n,&k);
while(k--)
scanf("%d",&a),t=t*s0*pw(sb*s0,a-ls-1),ls=a;
t=t*s0*pw(sb*s0,n-ls-1);
printf("%d\n",(t.v[0][1]+t.v[0][2])%mod);
}
AGC013F Two Faced Cards
Problem
有 个数对 以及 个限制 。 次询问,每次再给出一个数对 ,求出加入该数对后如下问题的答案(询问间独立):
你需要在每个数对中选出一个数 ,然后将这些 任意排列得到 ,满足 。定义一种方案的权值为选择 的数对数量。求出合法方案权值的最大值,或输出无解。
Sol
Sol 1(std):
问题较为复杂,从简单的子问题开始考虑。
首先考虑最后一步排列。显然的策略是将 同时从小到大排序后依次确定,但排序对于判定来说很困难。可以发现一个等效的判定方式是考虑将所有数放到数轴上,合法当且仅当能在数轴上向右匹配,那么这等价于括号匹配;因此考虑在 处 , 处 ,那么合法当且仅当数轴上任意一个前缀和非负。(也可以反过来考虑)
再考虑原问题的单组询问。相当于每个 可以放在两个位置中的一个,求合法前提下最多有多少个位置放在 。显然往左放对于合法判定更优,因此 的位置显然都放在 。为了处理最大值的问题,考虑将所有 的位置先都放在 或者都放在 ,然后求最多(最少)需要改多少个。
如果一开始都放在 ,可以发现相当于如下问题:
有若干个区间 ,你需要选出尽量多的区间。数轴上每个位置有被区间覆盖的次数限制。
但这不太好直接做。一种显然正确的策略是每次选择右端点最小的合法区间。另一种可行的方式是从左往右贪心,只考虑左侧限制,记录当前向右的候选区间的右端点。操作需要支持在一个选中区间结束时增加答案,或者加入区间,或者在覆盖次数改变时去掉右端点最大的候选区间。
但这些策略都不好处理询问:合法性会随着加入的 改变,加一个线段更影响合法性;对于第二种贪心,询问相当于在某一段前缀或后缀的时间中可以多保留一个数(覆盖次数上限 ),但这也会大幅度影响集合的结构(在很多轮操作之后)
考虑另一个方向,即一开始都放在 。那么变为相反的问题:每个位置的限制是至少覆盖多少次,要求选出尽量少的区间。
此时可以得到更简单的贪心:考虑左侧第一个需要被覆盖的位置,那么显然需要一个跨过它的区间。但此时它左侧不需要任何覆盖,那么只需要考虑区间右端点。因此可以得到如下策略:
每次选择包含第一个没有满足覆盖要求位置(左端点小于等于它)的区间中右端点最大的一个,覆盖这一段。如果有一步找不到区间则无解。(也可以倒过来做)
然后考虑多次询问。将询问的数对加进来显然过于复杂,考虑直接枚举加入的数对选择哪一个,然后加入一个固定的 。先默认 ,然后询问相当于一段前缀需要多覆盖一次。如果正着考虑,那显然需要多选包含开头位置的一个区间,但接下来会遇到问题:如果加的前缀小于这次选的区间,那这次操作在原问题中多覆盖了一段,但这就没法在原问题外处理了。
因此考虑倒过来做,变成加一段后缀。但此时的问题是需要考虑新选择的区间和原问题中选择区间的顺序。如果顺序考虑需要额外覆盖 开始的后缀一次的问题,那么考虑到 时就可能需要多覆盖一次,但有可能此时能选的最优区间在原问题中接下来被选了。假设后来 开始需要被覆盖的位置选择了这个区间,那么现在需要将这个区间拿过来,可以发现接下来 部分的限制和原问题一样,且这部分不会用到被拿走的区间,因此这部分过程和原问题一样。直到 的讨论时本来需要的区间被 拿走了,需要再拿一个区间。但可以发现之前部分和原问题这一部分拿出的区间集合是相同的,因此接下来就变成需要额外覆盖 开始的后缀的问题。
继续往后推导可以发现,可以直接跳过所有被多覆盖了的位置而不影响结果。那么只需要考虑从没有被多覆盖的位置开始的问题,这样就不存在之前的情况,从而选出一个区间不影响之后原问题的选择。设这次额外选出的区间是 ,那么可以发现这一段内部额外的一次被满足了,从而和原问题的操作一样。然后就变为从 开始需要额外覆盖的问题。那么考虑从后向前依次求出需要额外覆盖每个后缀一次的问题的答案。由之前的结论前一个问题在处理第一步后会变成后面的问题。那么维护当前剩余且位置满足条件的区间即可做到 。
显然这里可以离散化,那么就做到了 。离散化细节非常多:例如需要考虑各种边界开闭的顺序问题,寻找没有被覆盖的一段导致的 等问题(可能加到这里停止,那么第一个需要再覆盖的位置是 )。
Sol 2:
换一种方式考虑单组询问。按照从小到大的顺序考虑每个 。每个 可以匹配 的数对 ,且如果 则匹配有 的分数。
是否可用匹配只和 有关,权值只和 有关。考虑将数对的 和 一起排序并顺序考虑,维护当前可用的 。这样问题变为如下形式:
有 次操作,每次操作为如下形式之一:
- 加入一个数 。
- 给定 ,你需要拿出一个数,如果拿出的数小于等于 则有 的分数。每次给定的 递增。
因为考虑的 递增,可以发现一个数有贡献那么之后一定有贡献。这样只要一个数当前拿出来有贡献,那当前把这个数拿出来一定不差(考虑拿出别的数的情况,如果当前数之后没被选那么换这个数一定不差;否则考虑交换,当前数的贡献不受影响(都能 ),而另一个数之后再拿出也不会变差)。而如果当前没有数能产生贡献,那显然拿最大的最优(同样考虑对位)。那么有如下最优策略:
如果存在小于等于 的数那么拿任意一个,否则拿最大的 。
这样就可以 处理单组询问。接下来考虑多组询问的问题。
同样考虑枚举加入数对的情况,变为加入一个 的问题。显然这个 应该匹配右侧第一个 (显然 越大原问题越优,然后对位考虑)。那么问题可以看成给定 次拿出操作,对于每一个操作求出删去这个操作后原问题的答案。
仍然考虑从不删操作的原问题出发。但现在的一个问题是必须删一次操作才可能合法。因此先考虑合法性的问题。显然存在方案当且仅当不存在一次拿出操作时没有数。考虑保留 次拿出操作先顺序做,那么一定做到某个地方拿不出数。设这是第 次拿出操作,那可以发现删去后面的拿出操作后问题一定无解(到第 次就失败了);而删去一次前面的操作到这里一定有解,且前面操作的位置不会影响之后可用元素的个数。那么考虑删掉第 次操作继续做,如果之后再出问题则说明无论怎么删都无解,否则有解当且仅当删掉的是前 次之一。
此时 次之后的部分一定和之前无关(无论怎么操作到第 次后没有剩下的 ),那么只需要考虑前面的问题。
仍然尝试从一个问题推到另一个。改了一次操作后留下的元素会影响之后的状态,因此显然应该向后推。
以删掉第 次的情况为基准。考虑删去第 次的问题,相当于本来给第 次的元素被留了下来。考虑这个元素接下来会如何影响操作。因为这里元素没有数量限制,只需要考虑它什么时候能更优。之前的操作是能有分数就任意操作,否则选最大的。那么有两种情况:
- 它能将接下来的一次操作从没有分数变为有分数。
- 它能将接下来一次没有分数的操作中将更小的 替换下来。
可以发现找到下一次替换的操作 后,第 次操作本来选的元素就留了下来,那么之后就变成了一个删去第 次的问题,那么就可以递推了,只需要额外考虑这次变化带来的权值变化。
考虑如何找到 。第一种情况相当于找到接下来第一个没有分数且 大于等于某个值的操作,第二种相当于找到第一个没有分数且选择的 小于某个值的操作(在删去 的问题上)。因为值不是顺序的,可以 set
维护单调栈或者类似方式解决。
复杂度 ,但不需要考虑区间的细节。
Code
Sol 1:
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
#define N 100500
int n,q,a,b,c,si[N],ci,li=1e9+1,ct,as;
struct sth{int t,a;}op[N*4];
bool operator <(sth a,sth b){return a.t!=b.t?a.t>b.t:a.a>b.a;}
void solve()
{
sort(op+1,op+ct+1);
int nw=0;
set<int> v0;
multiset<int> rm;
multiset<pair<int,int> > v2;
for(int i=1;i<=ct;i++)
{
if(op[i].a>0)v2.insert(make_pair(op[i].a,op[i].t));
else if(op[i].a==0)nw++;
else if(op[i].a==-1)nw--;
if(op[i].t!=op[i+1].t)
while(rm.size()&&*rm.rbegin()>=op[i].t)rm.erase(rm.find(*rm.rbegin())),nw--;
while(nw<0)
{
if(v2.empty()){li=0;return;}
pair<int,int> vi=*v2.begin();v2.erase(v2.find(vi));
as--;nw++;rm.insert(vi.first);
while(v0.size()&&*v0.begin()<=vi.second)v0.erase(v0.begin());
}
if(!nw&&op[i].t!=op[i+1].t)v0.insert(op[i].t);
}
int la=0;
for(int i=ct;i>=1;i--)
{
if(op[i].a>0)v2.erase(make_pair(op[i].a,op[i].t));
else if(v0.count(op[i].t))
{
if(v2.empty()){li=la+1;return;}
int lb=(*v2.begin()).first;
if(lb>la){li=la+1;return;}
if(lb>=si[ci])si[++ci]=la+1;
}
if(op[i].t!=op[i+1].t)la=op[i].t;
}
li=la+1;
}
int calc(int x)
{
if(x>=li)return -2;
return as-(upper_bound(si+1,si+ci+1,x)-si-1);
}
int main()
{
scanf("%d",&n);as=n;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&b);
op[++ct]=(sth){a,-1};op[++ct]=(sth){b,-2};
if(a>b)op[++ct]=(sth){a,b};
}
for(int i=1;i<=n+1;i++)scanf("%d",&c),op[++ct]=(sth){c,0};
solve();
scanf("%d",&q);
while(q--)
{
scanf("%d%d",&a,&b);
int as=max(calc(a)+1,calc(b));
printf("%d\n",as>=0?as:-1);
}
}
Sol 2:
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
#define N 100500
int n,q,a,b,vl[N*2],as[N*2];
pair<int,int> si[N*2];
set<pair<int,int> > s0,s1,sr;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&a,&b),si[i]=make_pair(min(a,b),a);
for(int i=1;i<=n+1;i++)scanf("%d",&a),si[n+i]=make_pair(a,2e9);
scanf("%d",&q);
sort(si+1,si+n*2+2);
int fr=0,su=0;
for(int i=1;i<=n*2+1;i++)
if(si[i].second<1.5e9)sr.insert(make_pair(si[i].second,i));
else if(!sr.size())fr=fr?-1:i,vl[i]=-1;
else
{
pair<int,int> v1=*sr.begin();
if(v1.first>si[i].first)v1=*sr.rbegin(),su++;
sr.erase(v1);vl[i]=v1.second;
}
if(fr==-1){for(int i=1;i<=q;i++)printf("-1\n");return 0;}
for(int i=fr+1;i<=n*2+2;i++)as[i]=-1e9;
as[fr]=n-su;
for(int i=fr-1;i>=1;i--)
if(si[i].second<1.5e9)as[i]=as[i+1];
else
{
int nt=fr,nw=vl[i];
set<pair<int,int> >::iterator it=s0.lower_bound(make_pair(-si[nw].second+1,0));
if(it!=s0.end())nt=(*it).second;
it=s1.lower_bound(make_pair(si[nw].second,0));
if(it!=s1.end())nt=min(nt,(*it).second);
as[i]=as[nt]+(si[nw].second<=si[nt].first);
if(si[nw].second<=si[i].first)as[i]--;
else
{
s1.insert(make_pair(si[i].first,i));
pair<int,int> tp=make_pair(-si[nw].second,i);
while(s0.size())
{
pair<int,int> ls=*s0.begin();
if(ls.first<=tp.first)s0.erase(ls);
else break;
}
s0.insert(tp);
}
}
while(q--)
{
scanf("%d%d",&a,&b);
int v1=max(as[lower_bound(si+1,si+n*2+2,make_pair(a,0))-si]+1,as[lower_bound(si+1,si+n*2+2,make_pair(b,0))-si]);
printf("%d\n",v1<-1e7?-1:v1);
}
}
AGC012E Camel and Oases
Problem
数轴上有 个点,第 个点位置为 。
你有一个权值 。初始时你在一个点上,然后你可以进行如下两种操作:
- 移动到一个点。可以操作当且仅当两点距离不超过 。
- 移动到任意一个点,然后 变为 。可以操作当且仅当 。
给定固定的初始权值 。对于每个点,求从该点出发能否用上述两种操作经过所有点(可以重复经过点)。
Sol
在 不变的情况下,一个位置通过第一种操作能到达的位置显然是一段区间,且通过第一种操作过去之后一定能返回。那么显然有如下性质:
操作过程一定形如,先将 操作能走到的点全部经过,然后进行一次 操作,重复这一过程直到不能再进行 操作。
考虑每一轮 操作能到达的区间,那么一种合法的游走可以看成 个区间,其中第 个区间满足相邻两点距离不超过 。另一方面, 操作可以跳到任意点,那么任意一种选这么多区间的方式都可以被 操作连接起来。那么可以发现如下结论:
合法当且仅当可以找出 个区间覆盖所有点,其中第 个区间满足相邻两点距离不超过 。
先不考虑开头判定合法性。首先这个区间的条件满足单调性:如果一个区间可以是第 个,那它的子序列也可以是。因此相交部分可以直接删掉,从而如果有解必然存在方案满足所有区间两两不交(可以有区间不用,因为删相交时如果完全包含会直接去掉区间)。单调性也说明在两两不交的情况下,如果确定了区间之间的相对顺序则可以贪心放区间:显然每个位置尽量向右放最优(否则调整)。那么预处理第 个区间以 为左端点时能到的最大右端点,这可以做到 ,然后就可以 判断一种顺序有没有解。
枚举顺序的复杂度过大,但这里只需要考虑当前划分到了哪,之前选择的顺序不重要。那么考虑子集 dp:设 表示从左端点开始,用了 集合中的区间右端点最多能到多少。转移时枚举下一段选多少。复杂度 。
然后考虑每个位置出发的问题。可以发现从一个位置出发相当于限制第一段区间必须包含出发位置。可以发现确定出发位置后第一段的最优解可以唯一确定(向两侧尽量扩展)。同时显然不存在后面的一个区间能严格包含第一段(第一段两侧不能扩展的位置距离大于 ),那么可以将两侧分开,问题变为能否用剩下的区间覆盖一个前缀和一个后缀。
那么考虑前后缀分别做上述 dp,合法当且仅当存在一个子集 满足 。
这样单组询问是 的。但注意到一个区间可以一起处理,而区间的数量显然不会太多:如果有解,用 个更小限制的区间都可以覆盖整个序列,那么 个限制为 的区间也一定能覆盖整个序列。因此有解时第一步不同的区间不超过 个。
这里也可以不用这个结论:考虑在外面枚举 ,得到 。那么合法当且仅当它们中间能被第一段全部经过(如果这两个部分都相交了那显然任意起始点有解),因此只需要找到中间任意一个点所在的第一段,然后合法时这一段都整体合法。
复杂度
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 530001
int n,v,m,d[20],is[N],ci;
int s[2][N],nt[2][N][20],dp[2][N];
int main()
{
scanf("%d%d",&n,&v);
d[m=1]=v;while(d[m])d[m+1]=d[m]/2,m++;
for(int i=1;i<=n;i++)scanf("%d",&s[0][i]),s[1][n+1-i]=-s[0][i];
s[0][n+1]=s[1][n+1]=2e9;
for(int t=0;t<2;t++)
{
for(int i=n;i>=1;i--)
for(int j=1;j<=m;j++)
if(s[t][i+1]-s[t][i]>d[j])nt[t][i][j]=i;else nt[t][i][j]=nt[t][i+1][j];
for(int j=1;j<=m;j++)nt[t][n+1][j]=n;
for(int i=0;i<1<<m;i+=2)
for(int j=2;j<=m;j++)if((~i>>j-1)&1)
dp[t][i|(1<<j-1)]=max(dp[t][i|(1<<j-1)],nt[t][dp[t][i]+1][j]);
}
for(int i=1;i<=n;i++)
{
int r=nt[0][i][1];
int fg=0;
for(int j=0;j<1<<m;j+=2)if(dp[0][j]>=i-1&&dp[1][(1<<m)-2-j]>=n-r)fg=1;
if(fg)for(int j=i;j<=r;j++)is[j]=1;
i=r;ci++;if(ci==m)break;
}
for(int i=1;i<=n;i++)printf("%s\n",is[i]?"Possible":"Impossible");
}
AGC012F Prefix Median
Problem
给定一个长度为 的序列 。考虑对 进行如下操作得到长度为 的序列 :
是 的中位数。
你可以任意重排 ,求有多少种不同的 。答案模
Sol
考虑构造 的过程,每一步相当于加入两个数后求中位数。考虑中位数的变化,显然插入两个数后中位数要么不变要么变为它两侧的某一个数(如果两个数都插入在同一侧)。
考虑从这一角度分析。但如果有数相同,那么可能向两侧走后还能得到相同的方案,这太难了。因此先考虑简单的情况:先认为 两两不同,例如 。
回到原问题。统计合法方案数的常见方式是去重或者找 合法的条件。但这里去重显然很难,因此考虑判断一组 是否合法。如果顺向考虑,则可以确定每次插入的两个数在当前中位数 的哪一侧。同时每次 时,因为只能跳到两侧,所以 中间的数之前一定没有出现;而每个在 中出现的数之后一定会出现。但这种中间部分每个数是否出现的状态就很难避免:一种尝试是只记录中间有多少个数,在需要加入数( 变为一个新的值)时才加入;但每次跳过的部分限制它们只能在之后的操作被填上,限制还会覆盖,从而这样很难再判断剩下的位置能不能被填进去。
之前的问题实际上出在限制覆盖,即多次跳过同一个位置会覆盖限制,因此考虑换一个方向。倒着考虑这个问题。那么形式相当于已经有 个数,初始中位数在正中。每次操作删两个数,如果中位数两侧各删一个则中位数不变,否则中位数向另一个方向移动一个(如果删掉当前中位数也一样)。可以发现这样删数不会删到移动的一侧。然后考虑判定合法,如果 ,那么根据之前的结论这两个数中间的部分都应该在这次操作之前就被删掉,那么更小的 也不可能取到删掉的数,可以直接在接下来的过程中删掉它们。每一步要求一些数要在之前被删掉,那么考虑只做需要的操作,记录两侧分别有多少次删数没有用,移动时就把这一侧之前留下的操作拿来用。一个可能的问题是 跳的过程把之前在一侧的数移到了另一侧,但可以发现往一侧跳时另一侧会多出来两次操作,这两次操作已经可以处理移过去的一个数。因此不需要考虑这样的限制,只用记录可用的操作次数。显然记录每次实际向哪个方向跳了多少个数就可以还原 ,且这是一一对应(在 两两不同的情况下)。
那么考虑状态记录当前左侧有 个数,右侧有 个数,左侧还有 次删除操作,右侧还有 次删除操作。则转移形如:
- 不动,则两侧多一次操作,转移到
- 向左跳 个 ,那么在这一侧需要删掉 个数同时需要至少有 个数,同时给另一侧加两次操作,转移到 。
初始状态是 ,但这个状态数难以接受。考虑简化状态,可以发现实际上 没用:可以发现记当前是从后往前第 次操作,则一定有 (证明只需要观察转移然后归纳)。那么 不会限制操作。那么状态只需要记录当前操作次数 以及当前的 ,然后向上面一样转移。那么 两两不同的问题就做到了 。事实上这里的 也可以表示两侧可以跳到的数个数。
然后考虑有相同 的情况,显然将 排序后问题形式类似,但此时的问题在于可能不同跳的方式能得到相同的结果,例如跳到相同数上。考虑去重,直观上跳更远不如更近,那么可以想象如下结果:
如果对于某组 有合法方案,则存在一个方案满足对于每种数,如果它在第 个位置左侧则只用它的最右侧一个位置,如果它在第 个位置右侧则只用它左侧第一个位置,如果包含 则只用 。
证明:考虑左侧情况,显然第一次操作是从右侧跳过来(否则这一段直接被删掉了),那么考虑只跳到右边界,这样节省了一些左侧的删数操作,且即使接下来再向左跳也只需要把节省的操作用回去就行。另一侧的情况类似。
这样就解决了去重的问题,只需要限制每种数只能跳边界即可。但这样也带来了新的问题:如何判断两侧有多少可以跳的数。直接做看起来很难,但注意到之前有 ,那么可以发现如下结论:
在第 步操作时,可以跳到的区间一定对应原序列(不删数)的位置 。
证明: 的部分对应当前不能一次跳过去的部分。
那么第 次操作时,如果 ,那么左侧会多出一种可以选的数,否则可选的数种类不变。右侧同理。因此考虑将状态中的 改为当前这一侧还有多少种可以到的数。这样只在一些情况下两侧可以 。然后根据上一个结论,是否可以跳也不和每种数的数量有关,那么转移形式仍然和之前相同,只在是否 上可能会改变。
复杂度
事实上结合上述分析可以得到一个更优美的结论(但做法仍然是这样的 dp):
一种序列 合法当且仅当满足如下两个条件:
- 在排序后 的位置 中出现。(对应最后一个结论)
- 不存在 使得 (对应之前倒着做时跳过一个数就会删掉它)
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 105
#define mod 1000000007
int n,v[N],dp[N][N][N],as;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n*2-1;i++)scanf("%d",&v[i]);
sort(v+1,v+n*2);
dp[1][0][0]=1;
for(int i=1;i<n;i++)
for(int j=0;j<=n*2;j++)for(int k=0;k<=n*2;k++)if(dp[i][j][k])
{
int ls=j,rs=k;
if(v[n-i]!=v[n-i+1])ls++;
if(v[n+i]!=v[n+i-1])rs++;
dp[i+1][ls][rs]=(dp[i+1][ls][rs]+dp[i][j][k])%mod;
for(int l=0;l<ls;l++)dp[i+1][l][rs+1]=(dp[i+1][l][rs+1]+dp[i][j][k])%mod;
for(int l=0;l<rs;l++)dp[i+1][ls+1][l]=(dp[i+1][ls+1][l]+dp[i][j][k])%mod;
}
for(int j=0;j<=n*2;j++)for(int k=0;k<=n*2;k++)as=(as+dp[n][j][k])%mod;
printf("%d\n",as);
}
AGC011E Increasing Numbers
AGC011F Train Service Planning
AGC010E Rearranging
AGC010F Tree Game
AGC009D Uninity
AGC009E Eternal Average
AGC008E Next or Nextnext
AGC008F Black Radius
AGC007C Pushing Balls
AGC007E Shik and Travel
AGC007F Shik and Copying String
AGC006D Median Pyramid
AGC006E Rotate 3x3
AGC006F Blackout
AGC005E Sugigma: The Showdown
AGC005F Many Easy Problems
AGC004E Salvage Robots
AGC004F Namori
AGC003E Sequential operations on Sequence
AGC003F Fraction of Fractal
AGC002E Candy Piles
AGC002F Leftmost Ball
AGC001E BBQ Hard
AGC001F Wide Swap
-
-
-
- Submit
- AGC058F Authentic Tree DP
- AGC057C Increment or Xor
- AGC057D Sum Avoidance
- AGC057E RowCol/ColRow Sort
- AGC057F Reflection
- AGC056B Range Argmax
- AGC056D Subset Sum Game
- AGC056E Cheese
- AGC056F Degree Sequence in DFS Order
- AGC055C Weird LIS
- AGC055D ABC Ultimatum
- AGC055F Creative Splitting
- AGC054D (ox)
- AGC054E ZigZag Break
- AGC053C Random Card Game
- AGC053D Everyone is a Winner
- AGC053E More Peaks More Fun
- AGC052C Nondivisible Prefix Sums
- AGC052D Equal LIS
- AGC052E 3 Letters
- AGC052F Tree Vertices XOR
- AGC051C Flipper
- AGC051D C4
- AGC051E Middle Point
- AGC051F rng_58's Last Problem
- AGC050D Shopping
- AGC050E Three Traffic Lights
- AGC049D Convex Sequence
- AGC049E Increment Decrement
- AGC048D Pocky Game
- AGC048E Strange Relation
- AGC048F 01 Record
- AGC047D Twin Binary Trees
- AGC047E Product Simulation
- AGC047F Rooks
- AGC046D Secret Passage
- AGC046E Permutation Cover
- AGC046F Forbidden Tournament
- AGC045B 01 Unbalanced
- AGC045C Range Set
- AGC045D Lamps and Buttons
- AGC045E Fragile Balls
- AGC045F Division into Multiples
- AGC044C Strange Dance
- AGC044D Guess the Password
- AGC044E Random Pawn
- AGC044F Name-Preserving Clubs
- AGC043C Giant Graph
- AGC043D Merge Triplets
- AGC043E Topology
- AGC043F Jewelry Box
- AGC041D Problem Scores
- AGC041E Balancing Network
- AGC041F Histogram Rooks
- AGC040D Balance Beam
- AGC040E Prefix Suffix Addition
- AGC039D Incenters
- AGC039E Pairing Points
- AGC039F Min Product Sum
- AGC038E Gachapon
- AGC038F Two Permutations
- AGC037D Sorting a Grid
- AGC037E Reversing and Concatenating
- AGC037F Counting of Subarrays
- AGC036D Negative Cycle
- AGC036E ABC String
- AGC036F Square Constraints
- AGC035C Skolem Xor Tree
- AGC035D Add and Remove
- AGC035E Develop
- AGC035F Two Histograms
- AGC034D Manhattan Max Matching
- AGC034E Complete Compress
- AGC034F RNG and XOR
- AGC033D Complexity
- AGC033E Go around a Circle
- AGC033F Adding Edges
- AGC032C Three Circuits
- AGC032D Rotation Sort
- AGC032E Module Pairing
- AGC032F One Third
- AGC031D A Sequence of Permutations
- AGC031E Snuke the Phantom Thief
- AGC031F Walk on Graph
- AGC030C Coloring Torus
- AGC030D Inversion Sum
- AGC030E Less than 3
- AGC030F Permutation and Minimum
- AGC029E Wandering TKHS
- AGC029F Construction of a Tree
- AGC028C Min Cost Cycle
- AGC028D Chords
- AGC028E High Elements
- AGC028F Reachable Cells
- AGC028F2 Reachable Cells
- AGC027D Modulo Matrix
- AGC027E ABBreviate
- AGC027F Grafting
- AGC026D Histogram Coloring
- AGC026E Synchronized Subsequence
- AGC026F Manju Game
- AGC025D Choosing Points
- AGC025E Walking on a Tree
- AGC025F Addition and Andition
- AGC024D Isomorphism Freak
- AGC024E Sequence Growing Hard
- AGC024F Simple Subsequence Problem
- AGC023D Go Home
- AGC023E Inversions
- AGC023F 01 on Tree
- AGC022D Shopping
- AGC022E Median Replace
- AGC022F Checkers
- AGC021E Ball Eat Chameleons
- AGC021F Trinity
- AGC020D Min Max Repetition
- AGC020E Encoding Subsets
- AGC020F Arcs on a Circle
- AGC019E Shuffle and Swap
- AGC019F Yes or No
- AGC018E Sightseeing Plan
- AGC018F Two Trees
- AGC017C Snuke and Spells
- AGC017E Jigsaw
- AGC017F ZigZag
- AGC016D XOR Replace
- AGC016E Poor Turkeys
- AGC016F Games on DAG
- AGC015E Mr.Aoki Incubator
- AGC015F Kenus the Ancient Greek
- AGC014E Blue and Red Tree
- AGC014F Strange Sorting
- AGC013E Placing Squares
- AGC013F Two Faced Cards
- AGC012E Camel and Oases
- AGC012F Prefix Median
- AGC011E Increasing Numbers
- AGC011F Train Service Planning
- AGC010E Rearranging
- AGC010F Tree Game
- AGC009D Uninity
- AGC009E Eternal Average
- AGC008E Next or Nextnext
- AGC008F Black Radius
- AGC007C Pushing Balls
- AGC007E Shik and Travel
- AGC007F Shik and Copying String
- AGC006D Median Pyramid
- AGC006E Rotate 3x3
- AGC006F Blackout
- AGC005E Sugigma: The Showdown
- AGC005F Many Easy Problems
- AGC004E Salvage Robots
- AGC004F Namori
- AGC003E Sequential operations on Sequence
- AGC003F Fraction of Fractal
- AGC002E Candy Piles
- AGC002F Leftmost Ball
- AGC001E BBQ Hard
- AGC001F Wide Swap
- Submit
-
-