寒假训练营1

E dfs

被骗了,名字叫贪心,实际上是dfs。

注意到比赛场数只有10场,每场只有3种结果,爆搜复杂度也只有3^10,果断dfs。教训是数据小到能爆搜,就直接爆搜。

    if(x==m+1){//比完了
        int p0=p[0];
        sort(p.begin(),p.end(),cmp);
//        rep(i,0,p.size()-1)cout<<p[i]<<' ';
//        cout<<'\n';
        rep(i,0,p.size()-1){//计算排名
            if(p[i]==p0){
                ans=min(ans,i+1);
                return;
            }
        }
    }
    vector<int>q;
    q=p;
    int uu=u[x]-1,vv=v[x]-1;
    q[uu]+=3;//第一个人赢
    dfs(x+1,q);
    q=p;
    q[vv]+=3;//第二个人赢
    dfs(x+1,q);
    q=p;
    q[vv]++;//平局
    q[uu]++;
    dfs(x+1,q);
}
void solve(void){ 
    cin>>n>>m;
    vector<int>a;
    rep(i,1,n){
        int t;
        cin>>t;
        a.push_back(t);
    }
    rep(i,1,m){
        cin>>u[i]>>v[i];
    }
   ans=1e9; 
   dfs(1,a);
   cout<<ans<<'\n'; 
}

F 第二类斯特林数

m个数按位或结果是2^n-1,意味着右边前n位都至少有一个数该位为一。

任意两个数按位与等于0,意味着任意两个数都没有任意一位相同。那么右侧前n位每一位有且只有一个数该位为1,接下来就是这n个1在m个数里如何排列的问题了。

m个数是升序的,那么就只有升序排列一种排列方式。

m个数都不为0,意味着每个数至少有一个数位为1。

综上可以抽象成n个有区别的球(n个不同数位上的1),放到m个无区别的盒子里(m个数,由于只有一种排列,可以是求组合数,也就是无区别的),并且每个盒子都非空(每个数都不为0),这就是第二类斯特林数的定义。

具体就求法是1/(m!)* sigma((-1)^k * (m-k)^n * C(m,k)),(m-k)^n * C(m,k)表示选k个盒子空着,球随意装进剩余盒子中,(-1)^k表示用容斥选算出所有盒子非空的方案数,1/m!表示只算组合数

需要注意的是阶乘可以先预处理出来。

一个坑点是m可能比n大,那么一定有盒子是空的,但又要求所有数不为0,也就是所有盒子非空,那么此时方案数为0

{
    int ret = 1;
    while (n)
    {
        if (n & 1) ret = ret * a % mod;
        a = a * a % mod;
        n >>= 1;
    }
    return ret;
}
ll inv(ll x){
	return power(x, mod-2);
} 
void init(int N)//预处理阶乘和阶乘逆元的表
{
 
    fac[0] = 1;//预处理阶乘和阶乘的逆元
    for (int i = 1; i <= N; i++)
    {
        fac[i] = fac[i - 1] * i%mod;
    }
    ifac[N] = inv(fac[N]);//费马小定理
    for (int i=N - 1; i >= 0; i--)
    {
        ifac[i] = ifac[i + 1] * (i + 1)%mod;
    }
}
ll C(int m, int k)
{
    if (k < 0 || k >m  || m < 0) {//特判
        return 0;
    }
    return fac[m] * ifac[m - k] % mod * ifac[k] % mod;
}
void solve(void){ 
	int n,m;
    cin>>n>>m;
    init(m);
    ll ans=0;
    rep(k,0,m){
		if(k%2==0){
			ans=(ans+C(m,k)*power(m-k,n)%mod)%mod;
		} 
		else {
			ans=(ans-C(m,k)*power(m-k,n)%mod+mod)%mod;
		}
//        cout<<C(m,k)<<' '<<power(m-k,n)<<' '<<ans<<'\n';
    }
	cout<<ans*ifac[m]%mod;
}

H.数位操作

看似是背包,实际上是到数学。看不出来规律就先试几个例子,可以发现对于最大容量k,如果我们选其中一位1的位置i,那么所有cost的i位为0,更高位为k的子集,更低位随意的物品都能选,因为cost之间执行的操作是按位或,i的更高位都是k的子集,按位或之后也是k的子集,更低位虽然可能全为1,但是第i为为0了,更低位全为1也是小于k的,那么满足上述条件的cost全或起来也不会超过k。

那么我们可以枚举k为1的位置i,然后计算出每一个位置的最大value,再取最大值就是答案。

虽然思路有了,但是实现其实也不简单,遍历cost所有位判断这个物品能不能选,其实很容易出错。一个简单的做法是先构造一个i位为0,高位与k相同,低位全是1的数,然后依次判断cost是否是它的子集,如果是则该物品能选。

ll get(int x){
	ll ret=0;
	rep(i,1,n)
		if((x&w[i])==w[i])//如果是子集
        	ret+=v[i];//选上
	return ret;
}
void solve(void){ 
    cin>>n>>m;
    int t=m;
    rep(i,1,n)cin>>v[i]>>w[i];
    ll ans=get(m);//初始值
    rep(i,0,30)
    	if((m>>i)&1){
    		int k=((t>>(i+1))<<(i+1))|((1<<i)-1);//构造用于判断的数
			ans=max(get(k),ans);
		}
	cout<<ans<<'\n';
}

I.统计推断

没想到还会考这种题。其实题干不太严谨,统计推断只能得出最有可能的结果,而不能求出一个100%确定的结果,因此应该问“这组数据更可能是谁生成的”,而不是“这组数据是谁生成的”

显然第一种方法x,y是均匀的,但第二种方法,当x,y靠近边界时,更有可能被舍掉,因此越靠近边界密度越低。因此可以统计一下落在某个区间里的点数,如果和均匀分布的期望相差超过一定值,就可以判定是第二种,否则是第一种。

    int n,cnt=0;
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,y,r;
        cin>>x>>y>>r;
        if(abs(x)<=70&&abs(y)<=70)cnt++;//统计落在边长140的正方形内点数
    }
    int ans=(70.0*70)/(100*100)*1e5;//均匀分布下,落在该区域内的点数的期望
    if(abs(ans-cnt)>2000)cout<<"buaa-noob";//差的太多了则否定假设
    else cout<<"bit-noob";
}

K.基环树

看到关系,就应该想到建图,因为边可以表示关系。第i题的题干是:第ai题的答案是_。这实际上是在说i题和ai题的答案只要确定一个,另一个也就确定了,这就是一种关系,我们可以依此建图。

注意到每个题都只能决定一道其它题,也就是说把题看成点,所有点出度都为1,那么这张图一共n个点,n条边,实际上是一棵树再加了一条边,形成了一个环,也就是一颗基环树。

实际上只有位于环上的题的答案才是自由的,可枚举的,因为确定了环上答案后,链上答案都可以倒退唯一确定。因此对于一颗基环树,我们只需要枚举并检验环上有几种可行的答案,然后再把不同树的答案乘起来就是总方案数。

接下来就是具体实现,枚举并检验环上的答案,可以任选一点,枚举它的选项,对每一个选项,顺着往后推,直到再回到起点,看此时推出来的起点答案和枚举的答案是否相同,如果相同,这就是一个合法答案。

另一个问题是怎么找到环上一点。一种简单的方法是用并查集,因为环一定位于每个连通块的末端,那么只要每次合并并查集时都把后一个点的祖先作为祖先,最终整个连通块的祖先一定是环上一点。

    if(fa[x]==x)return x;
    return fa[x]=find(fa[x]);
}
void merge(int x,int y){
    int fx=find(x),fy=find(y);
    if(fx==fy)return;
    fa[fx]=fy;
}
int main(){
    int n;
    long long ans=1;
    int mod=998244353;
    cin>>n;
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        merge(i,a[i]);
        cin>>s[i];
    }
    for(int i=1;i<=n;i++){
        if(fa[i]==i){//祖先一定是环上一点
            int len = 1, l = a[i], sum = 0;
            while (l != i)//计算环长度
            {
                len ++;
                l = a[l];
            }
            for (int j = 0;j < 5;j ++)//枚举该点答案
            {
                int temp = j;
                for (int k = 1;k <= len;k ++)//递推一圈
                {
                    temp = s[l][temp] - 'A';
                    l = a[l];
                }
                if(temp == j) sum ++;//如果和枚举的相等,合法
            }
            ans = ans * sum % mod;//各个基环树答案数乘起来
        }
    }
    cout<<ans;
}