导语
第五次暑假训练,选取题整理,这波做的直接自闭
涉及的知识点
思维、数学、枚举
题目
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+1∣x2+y2,1≤x≤y≤n的正整数x,y的对数(即前者为后者整除)
思路:通过与参考文献中博主的讨论理解了答案的推得,网上找到的大多是直接使用了结论,在这里证明一下
已知
x
y
+
1
∣
x
2
+
y
2
xy+1|x^2+y^2
xy+1∣x2+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
x≤y
令
x
x
x为常数,
y
y
y为未知数,可得方程
y
2
−
k
x
y
+
x
2
−
k
=
0
y^2-kxy+x^2-k=0
y2−kxy+x2−k=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+k2x2−4(x2−k)
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=2kx−k2x2−4(x2−k)
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=x2−k
假设原方程的解
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,y2≤x且y2非负(满足方程的解都是非负)
带入
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,y2≤x,y2=kx−y1,此时把
y
2
y_2
y2看做
x
′
x'
x′,
x
x
x看做
y
′
y'
y′,带入原方程可以得到新的解
(
k
x
′
−
y
′
,
x
′
)
(kx'-y',x')
(kx′−y′,x′),迭代下去,最后可以获得一个最小的正整数解/0解作为迭代出口,迭代到小根(即
k
x
−
y
=
0
kx-y=0
kx−y=0)时,迭代结束,此时可以得到一组解(
x
,
x
3
x,x^3
x,x3),由这组解可以递推回去,下一组解为(
x
3
,
x
5
−
x
x^3,x^5-x
x3,x5−x)
关键点:k不变
模拟迭代的过程即可,给出的范围为 1 e 18 1e18 1e18,由最初的解( x , x 3 x,x^3 x,x3)可以知道,为确保在范围内有解,需要满足 x 3 ≤ 1 e 18 x^3\le 1e18 x3≤1e18
做题时可以先求出 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的算法,参考这两篇博客