2021牛客暑期多校训练营3

导语

第五次暑假训练,选取题整理,这波做的直接自闭

涉及的知识点

思维、数学、枚举

链接:2021牛客暑期多校训练营3

题目

B

题目大意:n×m的棋盘,每个位置有花费,正面为白,背面为黑,初始全为白,目标为全黑,对于任意两行和两列四个相交的位置,如果三个为黑,第四个可以不用花费而变黑,求全黑的最小花费

思路:最小生成树(愚昧的我真想不到 ),很容易能发现最少涂黑n+m-1个点就能使得全黑(有选择的前提下),题目条件其实可以转换为当一行两列或两行一列属于同一集合时,另外一行/列也可以收录进集合,那么原问题便转换为集合(并查集)+最小花费=Kruskal,模拟即可,更详细的解释如图

左图为最基本的情况,在左下角未填充的时候,左上角的行与列与右下角的行与列是隔断的,不处于同一集合,或者说连通块更为适合,在填充了左下角后,左上与右下连通,同时也使得右上的行与列连通,右图为更复杂的例子,请自行理解

在这里插入图片描述

代码

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N=3e7+10,M=5010;
int n,m,a,b,c,d,p,fa[2*M],ans;
struct node {
    int x,y;
    ll v;
    bool operator<(const node& a)const {
        return v<a.v;
    }
} A[N];
int Find(int x) {
    if(x==fa[x])
        return x;
    return fa[x]=Find(fa[x]);
}
void Kruskal() {
    int cnt=n+m;//注意数量
    sort(A+1,A+1+n*m);
    for(int i=1; i<=n*m; i++) {
        if(cnt==1)//避免无谓的查找,提前终止
            break;
        int fx=Find(A[i].x),fy=Find(A[i].y+n);
        if(fx!=fy) {
            cnt--;
            fa[fx]=fy;
            ans+=A[i].v;
        }
    }
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>n>>m>>a>>b>>c>>d>>p;
    A[0].v=a;
    for(int i=1; i<=n; i++)
        for(int j=1; j<=m; j++) {
            int id=(i-1)*m+j;
            A[id]= {i,j,((ll)A[id-1].v*A[id-1].v*b+(ll)A[id-1].v*c+d)%p};//矩阵一维化
        }
    for(int i=1; i<=n+m; i++)
        fa[i]=i;
    Kruskal();
    cout <<ans<<endl;
    return 0;
}

C

题目大意:给出一个矩阵,一开始为空,给出多个位置可以填入值(有的可以不填),并给出了每行每列的最大值必须满足多少,求填入的数值之和最小是多少

思路:参考直接照抄了清华的代码,首先,确定那些能够行列公用的点,也就是确定最值相等的行列,之后确定了一对后可以以列再确定行,行再确定列…,这样就贪心得出了在最大化利用最值相等的前提下,填入的点位置与和,之后对没有填到的行和列进行特判,用每个点来试,如果点对应行列其中一个无对应(没有填)并且该行/列最值小于另一个行/列,填上,以此类推,详见代码

代码

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn=2e3+10;
const int maxm=8e5+5;
int n,m,k,r[maxn],c[maxn],u[maxm],v[maxm],matchx[maxn],matchy[maxn];
bool vis[maxn];
ll ans;
vector<int>e[maxn];//对每一行存储同样最值的列
bool DFS(int u) {
    vis[u]=1;//标记已访问
    for(int i=0; i<e[u].size(); i++) {
        int to=e[u][i],mat=matchy[to];
        /*to为当前行的第i+1个对应的列,matchy[to]为该列已经匹配的行
        即最值相等*/
        if(!mat||(!vis[mat]&&DFS(mat))) {
            matchy[to]=u;//形成对应关系,列对应行
            matchx[u]=to;//形成对应关系,行对应列
            return 1;
        }
        /*mat为0代表该列无对应的行,vis标记当前行是否已经使用过了
        如果没有使用,遍历判断能否使用
        使用的定义:在这一行存在对应位置的列能使用
            */
    }
    return 0;
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>n>>m>>k;
    for(int i=1; i<=n; i++)
        cin >>r[i];
    for(int i=1; i<=n; i++)
        cin >>c[i];
    for(int i=1; i<=m; i++) {
        cin >>u[i]>>v[i];//输入可填的横纵坐标
        if(r[u[i]]==c[v[i]])//如果对应行列的最值相等,存入
            e[u[i]].push_back(v[i]);
    }
    for(int i=1; i<=n; i++) { //搜每一行,构造出所有的对应关系
        memset(vis,0,sizeof(vis));
        DFS(i);
    }
    for(int i=1; i<=n; i++)//对每一行判断是否已经形成对应关系,形成直接累和即可
        if(matchx[i])
            ans+=r[i];
    for(int i=1; i<=m; i++) {//对没有形成对应关系的未定义点进行判断
        if(r[u[i]]<=c[v[i]]&&!matchx[u[i]]) {//如果该点的行无对应且行需要的值小于列,先判断行
            ans+=r[u[i]];//取最小值
            matchx[u[i]]=1;//标记
        }
        if(r[u[i]]>=c[v[i]]&&!matchy[v[i]]) {//如果该点的列无对应且列需要的值小于行,行已经判断了
            ans+=c[v[i]];
            matchy[v[i]]=1;
        }
    }
    cout <<ans<<endl;
    return 0;
}

E

题目大意:给出一个数n,统计满足 x y + 1 ∣ x 2 + y 2 , 1 ≤ x ≤ y ≤ n xy+1|x^2+y^2,1\le x\le y\le n xy+1x2+y2,1xyn的正整数x,y的对数(即前者为后者整除)

思路:通过与参考文献中博主的讨论理解了答案的推得,网上找到的大多是直接使用了结论,在这里证明一下


已知 x y + 1 ∣ x 2 + y 2 xy+1|x^2+y^2 xy+1x2+y2
不妨设存在 k ( x y + 1 ) = x 2 + y 2 k(xy+1)=x^2+y^2 k(xy+1)=x2+y2 x ≤ y x\le y xy
x x x为常数, y y y为未知数,可得方程 y 2 − k x y + x 2 − k = 0 y^2-kxy+x^2-k=0 y2kxy+x2k=0
由韦达定理得
y 1 = k x + k 2 x 2 − 4 ( x 2 − k ) 2 y1=\frac{kx+\sqrt{k^2x^2-4(x^2-k)}}{2} y1=2kx+k2x24(x2k)
y 2 = k x − k 2 x 2 − 4 ( x 2 − k ) 2 y2=\frac{kx-\sqrt{k^2x^2-4(x^2-k)}}{2} y2=2kxk2x24(x2k)
y 1 + y 2 = k x , y 1 y 2 = x 2 − k y_1+y_2=kx,y_1y_2=x^2-k y1+y2=kx,y1y2=x2k
假设原方程的解 y = y 1 y=y_1 y=y1,由 y 1 , y 2 y1,y2 y1,y2表达式可知 y 1 > y 2 , y 2 ≤ x 且 y 2 非 负 ( 满 足 方 程 的 解 都 是 非 负 ) y_1>y_2,y_2\le x且y_2非负(满足方程的解都是非负) y1>y2,y2xy2()
带入 y 2 y_2 y2 k ( x y 2 + 1 ) = x 2 + y 2 2 , y 2 ≤ x , y 2 = k x − y 1 k(xy_2+1)=x^2+y_2^2,y_2\le x,y_2=kx-y_1 k(xy2+1)=x2+y22,y2x,y2=kxy1,此时把 y 2 y_2 y2看做 x ′ x' x x x x看做 y ′ y' y,带入原方程可以得到新的解 ( k x ′ − y ′ , x ′ ) (kx'-y',x') (kxy,x),迭代下去,最后可以获得一个最小的正整数解/0解作为迭代出口,迭代到小根(即 k x − y = 0 kx-y=0 kxy=0)时,迭代结束,此时可以得到一组解( x , x 3 x,x^3 x,x3),由这组解可以递推回去,下一组解为( x 3 , x 5 − x x^3,x^5-x x3,x5x)
关键点:k不变


模拟迭代的过程即可,给出的范围为 1 e 18 1e18 1e18,由最初的解( x , x 3 x,x^3 x,x3)可以知道,为确保在范围内有解,需要满足 x 3 ≤ 1 e 18 x^3\le 1e18 x31e18

做题时可以先求出 1 e 6 1e6 1e6内范围内的所有解个数,然后二分查找即可

代码

#include <bits/stdc++.h>

using namespace std;
const int maxn=1e6+5
typedef long long ll;
ll a[maxn],t,n,N=1e18,cnt;
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>t;
    for(ll i=2; 1ll*i*i*i<=N; i++) {//尝试把每个值带进去,构造解的值
        ll x=i,y=i*i*i;//y为大根
        while(y<=N) {
            a[++cnt]=y;
            if((N+x)/i/i<y)//终止条件,tmp大于N
                break;
            ll tmp=y*i*i-x;//构造下一组解
            x=y;
            y=tmp;
        }
    }
    sort(a+1,a+1+cnt);//对所有解排序,方便统计
    while(t--) {
        cin >>n;
        cout <<upper_bound(a+1,a+1+cnt,n)-a<<endl;
    }
    return 0;
}

F

题目大意:24点的游戏,求出给定数字是否能计算出给定值,并且计算的过程中必定出现分数,如果有无分数解法或得不出给定值,舍弃,如果能计算出给定值并且计算中有分数,统计数量并输出

思路:北大人还是北大人…参考直接照抄了逆十字的代码,具体思想见代码,考虑所有的情况如果出现了无分数解法或无解直接返回,否则计数

代码

#include <bits/stdc++.h>

using namespace std;
int n,m,ans,ansn;
vector<double>nw;
vector<int>cnt[1000010],nww;
int solve(vector<double>nw) {
    if(nw.size()==1) {//只剩下一个数的时候
        if(fabs(nw[0]-m)<1e-9)//如果计算出来的结果与给定值相等
            return 2;
        return 0;
    }
    int sz=nw.size(),ans=0;
    for(int i=0; i<sz; i++)
        for(int j=i+1; j<sz; j++) {
            vector<double>Nw;
            Nw.clear();//清空
            for(int k=0; k<sz; k++)//存储除了操作数之外的值
                if(k!=i&&k!=j)
                    Nw.push_back(nw[k]);

            Nw.push_back(nw[i]+nw[j]);//尝试计算加
            ans=max(solve(Nw),ans);//递归,计算加上之后的情况
            if(ans==2)//存在无分数解,返回2
                return 2;

            Nw.pop_back();
            Nw.push_back(nw[i]-nw[j]);//尝试计算减
            ans=max(solve(Nw),ans);//递归,计算减去之后的情况
            if(ans==2)
                return 2;

            Nw.pop_back();
            Nw.push_back(nw[i]*nw[j]);//尝试计算乘
            ans=max(solve(Nw),ans);//递归,计算乘上之后的情况
            if(ans==2)
                return 2;

            Nw.pop_back();
            Nw.push_back(nw[j]-nw[i]);//尝试计算减
            ans=max(solve(Nw),ans);//递归,计算减去之后的情况
            if(ans==2)
                return 2;

            Nw.pop_back();
            if(fabs(nw[j])>1e-9) {//尝试除,分母不能为0
                Nw.push_back(nw[i]/nw[j]);
                int nww=2;
                if(nw[i]/nw[j]-floor(nw[i]/nw[j]+1e-8)>1e-9)//判断分数
                    nww=1;
                ans=max(ans,min(nww,solve(Nw)));
                if(ans==2)
                    return 2;
                Nw.pop_back();
            }
            if(fabs(nw[i])>1e-9) {//尝试除,分母不能为0
                Nw.push_back(nw[j]/nw[i]);
                int nww=2;
                if(nw[j]/nw[i]-floor(nw[j]/nw[i]+1e-8)>1e-9)//判断分数
                    nww=1;
                ans=max(ans,min(nww,solve(Nw)));
                if(ans==2)
                    return 2;
            }
        }
    return ans;
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>n>>m;
    for(int i=1; i<=13; i++)//尝试每个数字
        for(int j=i; j<=(n>=2?13:i); j++)//如果可使用数大于等于2
            for(int k=j; k<=(n>=3?13:j); k++)
                for(int l=k; l<=(n==4?13:k); l++) {
                    nw.clear(),nww.clear();
                    nw.push_back(i),nww.push_back(i);
                    if(n>1)
                        nw.push_back(j),nww.push_back(j);
                    if(n>2)
                        nw.push_back(k),nww.push_back(k);
                    if(n>3)
                        nw.push_back(l),nww.push_back(l);
                    //存入对应的个数
                    if(solve(nw)==1)//如果有解并且全为分数解
                        cnt[++ansn]=nww;//存符合条件的结果
                }
    cout <<ansn<<endl;
    for(int i=1; i<=ansn; i++)
        for(int j=0,sz=cnt[i].size(); j<sz; j++)
            cout <<cnt[i][j]<<(j==sz-1?'\n':' ');
    return 0;
}

J

题目大意:一个无向完全图,n个节点,每条边黑白两色,询问能选出三边颜色相等的三元环多少个

思路:写的时候尝试了 m m m\sqrt m mm 的算法,显然过不了。正难则反,对于给出的图,首先求出图中所有的三元环个数,然后减去不符合条件的三元环,即存在异色边的三元环,存在异色边,代表存在一个点引出两条异色边的情况,对每个点延伸的每条边找先前已经遍历过的与其颜色互异的边,累和,如图,当计算到红色线时便需要累和先前的黑色线个数。

其次,每个不合法三元环都被重复计算了一次,因为异色边的两点都进行了一次拓展,最后累和需要除以2

在这里插入图片描述

代码

#include <bits/stdc++.h>
namespace GenHelper {
unsigned z1,z2,z3,z4,b,u;
unsigned get() {
    b=((z1<<6)^z1)>>13;
    z1=((z1&4294967294U)<<18)^b;
    b=((z2<<2)^z2)>>27;
    z2=((z2&4294967288U)<<2)^b;
    b=((z3<<13)^z3)>>21;
    z3=((z3&4294967280U)<<7)^b;
    b=((z4<<3)^z4)>>12;
    z4=((z4&4294967168U)<<13)^b;
    return (z1^z2^z3^z4);
}
bool read() {
    while (!u)
        u = get();
    bool res = u & 1;
    u >>= 1;
    return res;
}
void srand(int x) {
    z1=x;
    z2=(~x)^0x233333333U;
    z3=x^0x1234598766U;
    z4=(~x)+51;
    u = 0;
}
}
using namespace GenHelper;//默认代码
using namespace std;
typedef long long ll;
bool edge[8005][8005];
unordered_set<ll>SB,SW;
int main() {
    int n, seed;
    scanf("%d%d",&n,&seed);
    srand(seed);
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++)
            edge[j][i] = edge[i][j] = read();
    ll ans=1LL*n*(n-1)*(n-2)/6,res=0;//求出所有的三角形数量
    for(int i=0; i<n; i++) {
        int b=0,w=0;
        for(int j=0; j<n; j++) {
            if(i==j)
                continue;
            if(edge[i][j]) {
                res+=w;
                b++;
            } else {
                res+=b;
                w++;
            }
        }
    }
    printf("%lld\n",ans-res/2);
    return 0;
}

拓展:计算图中的三元环

m m m\sqrt m mm 的算法,参考这两篇博客

黑科技之三元环讲解

无向图求三元环

参考文献

  1. CSUST 4007-你真的会图论吗?(思维-三元环)
  2. 2021牛客暑期多校训练营3题解讨论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值