【Codeforces】【图论】【数量】【哈密顿路径】Fake bullions (CodeForces - 804F)
题意
有n个黑帮(gang),每个黑帮有siz[i]个人,黑帮与黑帮之间有有向边,并形成了一个竞赛完全图(即去除方向后正好为一个无向完全图)。在很多年前,有一些人参与了一次大型抢劫,参与抢劫的人都获得了一个真金条。
在这些年间,不同的黑帮之间进行了交易。具体过程是:
在时刻i,假如有一条边是u->v,那么u帮派中的i mod siz[u]号如果有金条(无论真假),并且v帮的i mod siz[v]没有任何金条,那么u中的这个人就会向v中的这个人一个假金条。
经过无数年的交易之后,各个帮派的金条拥有情况就确定了。他们开始向外面的世界倾销金条。真金条一定能够卖出去,而假金条可能买的出去,也可能卖不出去。定义一个帮派的力量值(strength)为成功卖出去的金条的数量。将所有的帮派按照力量值从大到小进行排序,从前a个帮派中选b个帮派。求所有可能的选出来的集合的个数。答案模1e9+7。
1<=b<=a<=n<=510^3
siz[i]<=210^6,\(\sum{siz[i]}<=2*10^6\)
输入格式
第一行为n,a,b。
接下来是一个n*n的邻接矩阵,无自环,(i,j)为1表示有边i->j,且G[i][j]+G[j][i]=1.
接下来有n行,每行开头是一个整数siz[i],接下来一个长为siz[i]的二进制串,如果第j(0 <=j< siz[i])位是1表示第j个人最开始有真金条,否则最开始没有金条。
输出格式
一个整数表示答案。
思路
首先原题的题目打错了,并不是0表示有金条,而是1表示有金条...虽然CF发了更正通知,但是题目并没有更改过来...
这道题显然是两部分:一个是求最后的金条拥有情况,一个是根据帮派的最大销售量mx[i]和最小销售量mn[i]求答案。
第一部分
首先有几个性质:
性质一
对于u->v这条边而言,u中的i能够对v中的j造成贡献,当且仅当\(i \equiv j(mod\) \(gcd(siz[u],siz[v]))\)(具体的证明博主表示不想说了,大概就是欧几里得搞一下)。
并由此能够推出一个引理,就是对于一条路径:u->p1->p2->p3->...->v,u中的i能够对v中的j造成贡献,当且仅当\(i \equiv j(mod\) \(gcd(siz[u],siz[p1],siz[p2]...siz[v]))\)(具体的证明等我看懂了再补吧...)
因此一个黑帮对另一个黑帮造成贡献时,当然是经过的中间点越多越好。
性质二
对于一条链的情况上面已经说过了,不难想到对于一个强连通分量而言,假设其中所有的黑帮的siz的gcd为bgcd(block gcd),那么其中的某一个黑帮的i号有金条当且仅当\(i \equiv j(mod\) \(bgcd)\),且j(另外一个黑帮的成员)有金条。
那么我们就可以将整个强连通分量看做是一个整体(即一个黑帮),一共有bgcd个人,其中第i个人有金条当且仅当\(i \equiv j(mod\) \(bgcd)(j是这个块中的某一个黑帮中的成员且j有金条)\)。
这样缩点之后我们就得到了一个有向无环的竞赛完全图。
性质三
假设我们处理完之后,怎么通过一个强连通分量在外部得到的信息来更新块内的信息呢??
这个还是比较好想的。
假设num[i]为第i个黑帮最后拥有的真假金条的总和。设它属于第k个块。这个块的虚拟黑帮最后有blonum[k]个真假金条。那么有:
这样就可以只看最后的那个DAG了。
性质四
在得到的DAG中,一定存在着一条哈密顿路径。因为假设不存在,那么一定有大于等于2个点,它们的出度为0。但是由于这是一个竞赛完全图,两两之间都有连边,因此这是不可能的。那么就存在了一条哈密顿路径。
其次,在这条哈密顿路径上,从起点开始,出度递减,入度递增。
这是因为加入存在一个点的出度大于它之前的那个点,那么它必然能够指向之前那个点的一条入边的来源点(因为当前这个点连向的点的数量大于前面的点的入边的来源点的数量),这样就形成环了(众所周知,环是一个强连通分量)。
又因为入边+出边=n-1,所以对于入边也是成立的。
性质五
这是和那个DAG有关的。沿着那条哈密顿路径,其中的点i只能够对之后的点造成贡献,而不能够先回到之前的点,因为这样子也有环了。
又因为一个黑帮对另一个黑帮造成贡献时,经过的中间点越多越好,所以只能够沿着哈密顿路径进行贡献。
并且哈密顿路径的起点到终点的新分配的强联通编号是递减的(从dfs树上就能看出)。
做法
有了以上的性质之后,就好搞了。
首先进行Tarjan缩点,对于每一个黑帮的强连通分量建立一个虚拟黑帮。然后将第i个黑帮的金条信息利用性质二放进去。
然后就得到了一个DAG。我们在这个DAG中找出一条哈密顿路径。然后从起点开始,不断地将当前强联通分量的信息向着下一个点进行转移(记住每一次转移的剩余系都是在mod gcd(blo[i],blo[i-1]下的)),然后依次做下来,就是O(n^2)的了。
这样子就得到了最终的金条分别情况了。心累
第二部分
假设i号黑帮的最大销售量为mx[i],最低为mn[i]。那么我们枚举i,并且考虑i号黑帮是选出来的b个黑帮中最差劲(力量值最低)的那个。
首先,对于mn[j]>mx[i]的j号,他肯定是要排在i号前面的,先统计起来为cnt1.然后就是mn[j]<=mx[i]的,由于i号是最差的为了防止重复,我们定义“最差”为:力量值为唯一最小或(不唯一最小,但是编号最小)。那么我们就统计这些mn[j]<=mx[i]且(mx[j]>mx[j]||(mx[j]==mx[i]&&j>i))的数量cnt2.
然后我们考虑枚举b中有j个人来自cnt2,那么来自cnt1的就有b-j-1个人。
然后对于j有一些限制:
1.j<=cnt2
2.j<=a-1-cnt1
3.j>=0
4.j+1+cnt1>=b
限制的具体原因就不提及的,请聪明的读者自行死尻思考。
然后将C(cnt2,j)*C(cnt1,b-1-j)累加起来就好了。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define MAXN 10000
#define MAXL 2000000
#define MO 1000000007
using namespace std;
int n,a,b,c;
char str[MAXL+5];
vector<int> G[MAXN+5];
vector<int> per[MAXN+5];
int siz[MAXN+5],dfn[MAXN+5],low[MAXN+5],dcnt;
int blo[MAXN+5],bgcd[MAXN+5],blocnt;
int stk[MAXN+5],t;
int num[MAXN+5],mx[MAXN+5],mn[MAXN+5];
bool instk[MAXN+5];
int fact[MAXN+5],inv[MAXN+5];
int PowMod(int x,int y)
{
int ret=1;
while(y)
{
if(y&1)
ret=1LL*ret*x%MO;
x=1LL*x*x%MO;
y>>=1;
}
return ret;
}
int gcd(int x,int y)
{
if(y==0)
return x;
return gcd(y,x%y);
}
void InPut()
{
scanf("%d %d %d",&n,&a,&b);
for(int i=1;i<=n;i++)
{
scanf("%s",str+1);
for(int j=1;j<=n;j++)
if(str[j]=='1')
G[i].push_back(j);
}
for(int i=1;i<=n;i++)
{
scanf("%d",&siz[i]);
per[i].resize(siz[i]+1);
scanf("%s",str);
for(int j=0;j<siz[i];j++)
if(str[j]=='1')
per[i][j]=1;
else if(str[j]=='0')
per[i][j]=0;
}
}
void Tarjan(int u,int fa)
{
dfn[u]=low[u]=++dcnt;
instk[u]=true;stk[t++]=u;
for(int i=0;i<(int)G[u].size();i++)
{
int v=G[u][i];
if(dfn[v]==0)
{
Tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else if(instk[v]==true)
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
int fro=0;blocnt++;
do
{
fro=stk[--t];instk[fro]=false;
blo[fro]=blocnt;
bgcd[blocnt]=gcd(bgcd[blocnt],siz[fro]);
}while(fro!=u);
}
}
void Part1()
{
for(int i=1;i<=n;i++)
if(dfn[i]==0)
Tarjan(i,-1);
for(int i=1;i<=blocnt;i++)
per[i+n].resize(bgcd[i]+1);//预先设定大小
for(int i=1;i<=n;i++)
for(int j=0;j<siz[i];j++)
per[blo[i]+n][j%bgcd[blo[i]]]|=per[i][j];//存到虚拟黑帮中
int nw;
for(int i=blocnt;i>1;i--)//自然形成了哈密顿路径
{
nw=gcd(bgcd[i],bgcd[i-1]);//当前的剩余系
for(int j=0;j<bgcd[i];j++)
if(per[i+n][j])
{
per[i-1+n][j%nw]|=1;//对下一个点进行更新
num[i]++;//num就是总的金条数
}
}
for(int j=0;j<bgcd[1];j++)
num[1]+=per[1+n][j];//记得一定要统计block1的值
for(int i=1;i<=n;i++)
{
mx[i]=mn[i]=0;
mx[i]=1LL*num[blo[i]]*siz[i]/bgcd[blo[i]];根据性质三进行还原
for(int j=0;j<siz[i];j++)
mn[i]+=per[i][j];//直接利用初始信息算
}
}
void Prepare()
{
fact[0]=1;
for(int i=1;i<=MAXN;i++)
fact[i]=1LL*fact[i-1]*i%MO;
inv[MAXN]=PowMod(fact[MAXN],MO-2);
for(int i=MAXN-1;i>=0;i--)
inv[i]=1LL*inv[i+1]*(1LL*i+1LL)%MO;
}
inline int C(int x,int y)
{
return 1LL*fact[x]*inv[y]%MO*inv[x-y]%MO;
}
int Part2()
{
Prepare();
int cnt1=0,cnt2=0,ret=0;
for(int i=1;i<=n;i++)
{
cnt1=cnt2=0;
for(int j=1;j<=n;j++)
{
if(i!=j&&mn[j]>mx[i])
cnt1++;
if(i!=j&&mn[j]<=mx[i]&&(mx[j]>mx[i]||(mx[j]==mx[i]&&j<i)))//避免重复
cnt2++;
}
if(cnt1>a-1)
continue;
for(int j=min(b,min(a-1-cnt1,cnt2));j>=0&&j+1+cnt1>=b;j--)//一堆限制
ret=(1LL*ret+1LL*C(cnt2,j)*C(cnt1,b-j-1)%MO)%MO;
}
return ret;
}
int main()
{
InPut();
Part1();
int ans=Part2();
printf("%d\n",ans);
return 0;
}
值得一提的是,为了迎合题目的限制,这里我使用了vector(赖皮)
可能有些地方并非原创,部分借鉴了网上的题解和CF的官方题解。
但是题解(除了性质一)绝对是详细的。
祝您玩的愉快