这场神中神,题型都很新,学到了很多。比赛链接,官方视频讲解,出题人题解
这场官方讲解我觉得讲的还是很好的。
D是个不太裸的DP,是01背包的变种。
E有三种做法,在前两天的abc(atcoder beginner contest)出过一个弱化的一模一样的题,这题正解是逆推(题解说这个是并查集,感觉不像,顶多算沾点思想),次正解是个启发式合并,我赛时瞎搞出来一个并查集做法,也一并拿过来说了。
F是个暴力,不过需要看出来一个结论来减枝。这题有个超级涩情 nb的结论,我是不会证的。也没看到有讲的。有大佬会的话希望能不吝赐教。
G是个计算几何。之前没做过,用的高中知识瞎搞出来了。正解是用向量,可以向量旋转,再用个正弦定理就完事了。神!
A 超级闪光牛可乐
思路:
一定至少有一种零食,而这种零食至少有 1 1 1 点的诱惑力。最多能吃 1000 1000 1000 袋,再怎么说也够用了,所以直接输出 1000 1000 1000 次第一种零食就完事了。
code:
#include <iostream>
#include <cstdio>
using namespace std;int x,n;
char ch;int main(){cin>>x>>n>>ch;for(int i=1;i<=1000;i++)cout<<ch;return 0;
}
B 人列计算机
思路:
每一行的字符串有可能不相连,因此需要整行读入字符串,用 g e t l i n e getline getline 就行了。这个本来是 i s t r e a m istream istream 类中的一个成员函数,用于读入字符串并返回首字符指针,在 c s t r i n g cstring cstring 中重载了一下,可以读入 s t r i n g string string。
然后根据一些不同的信息判定是哪个门的一种,提取输入信息。然后输出结果即可。
code:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;string s[10];
bool x,y;int main(){for(int i=1;i<=5;i++)getline(cin,s[i]);if(s[3].substr(4,3)==" & "){x=s[2][0]=='1';y=s[4][0]=='1';cout<<(x&y);}else if(s[3].substr(4,3)==">=1"){x=s[2][0]=='1';y=s[4][0]=='1';cout<<(x|y);}else {cout<<(s[3][0]=='0');}return 0;
}
C 时间管理大师
思路:
众所周知, 24 24 24 时制本质是个 60 60 60 进制数,所以可以把它转化为 10 10 10 进制数(其实就是把时,分都拆成秒,不过这题没用到秒,所以时拆成分就够用了),再把 0 0 0 时 0 0 0 分看作基准时刻(也就是 0 0 0 时刻)。这样就可以方便的找到某个时刻前 1 , 3 , 5 1,3,5 1,3,5 分钟时刻对应的 10 10 10 进制数了,用 s e t set set 去一下重,再转化为 60 60 60 进制数输出即可。
code:
#include <iostream>
#include <cstdio>
#include <set>
using namespace std;int n;
set<int> s;int main(){cin>>n;for(int i=1,tm,tmp;i<=n;i++){cin>>tmp;tm=tmp*60;cin>>tmp;tm+=tmp;s.insert(tm-1);s.insert(tm-3);s.insert(tm-5);}cout<<s.size()<<endl;for(auto x:s){cout<<x/60<<" "<<x%60<<endl;}return 0;
}
D 我不是大富翁
思路1:
先讲一下本人丑陋的做法。先把环拉直, 1 ∼ n 1\sim n 1∼n 看成是数轴上的 0 ∼ n − 1 0\sim n-1 0∼n−1,顺时针走就相当于向右走,逆时针走就相当于向左走,循环就相当于取模。因为所有数都要用到,所以相当于取某些数为正,某些数为负,它们的和等于 n n n 的倍数,这样一取模就回到了原点。
正数与负数之和等于 n n n 的倍数,相当于正数绝对值之和与负数绝对值之和模 n n n 同余。所以我们找到正数之和的所有可能的余数,然后枚举余数, n n n 减去这个余数模 n n n 就得到了负数绝对值之和的余数了,如果它与正数的余数同余,就说明有解。如果找一遍下来都不满足条件,就说明无解。
找正数之和所有可能的余数,这个过程就是个01背包了,不过下一个状态的位置需要对 n n n 取模,这样可能会导致下一个状态的位置不一定在这个状态后面,如果用一维有可能会干扰到前面状态的枚举,因此需要开多维,或者使用临时数组来存储(滚动数组 )。
code1:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=5005;int n,m;
long long tot;
bool dp[maxn],t[maxn];int main(){cin>>n>>m;dp[0]=true;for(int i=1,a;i<=m;i++){cin>>a;tot+=a;for(int j=0;j<n;j++){if(dp[j])t[(j+a)%n]=t[j]=1;}for(int j=0;j<n;j++){dp[j]=t[j];t[j]=0;}}for(int i=0;i<n;i++){if(dp[i] && i==(tot-i)%n){puts("YES");return 0;}}puts("NO");return 0;
}
思路2:
官方讲解的思路,就是把一个数的正负两种状态都看作一件物品来跑01背包,不允许不拿物品。跑一遍之后直接看 d p [ m ] [ 0 ] dp[m][0] dp[m][0] 是否为 1 1 1 即可。其他的处理还是一样的
code2:
E 多重映射
思路1:
和abc的这场的C题一模一样,这题的数据量更大,题解的思路2本质上是个合并的思想,再加个合并小的数组的思想,就变成本题的次正解 启发式合并的思路了。
发现我的做法和题解的做法思维模型有点相似,先讲我现场yy的神秘做法了。
不难看出这个题不能简单粗暴的用值来并查集,不然先把 2 2 2 变成 3 3 3,再把 2 2 2 变成 1 1 1,这样 123 123 123 都变成了 3 3 3,但是答案应该是 1 1 1 是 2 2 2, 23 23 23 是 3 3 3。但是它应该是在合并某些东西的。
这里合并的应该是位置。某些位置有相同的某个数 a a a,把等于 a a a 的所有位置看成一个集合,当我们把其他的一个数 b b b 变成 a a a 的时候,就相当于将等于 b b b 的所有位置集合合并到等于 a a a 的所有位置集合上,在并查集中只需要把代表元素合并一下即可,这个过程需要查询等于 b b b 的位置集合的代表元素。而具体等于什么数,相当于这个集合的一个描述信息,直接在代表元素上记录一下即可。
其实想一下这个合并过程,就像一棵树一样,每个节点是个集合,也就是 某个数的位置集合,一直从叶子节点合并到根节点。暴力会超时就是因为节点的合并是集合的合并,最坏情况每次合并都是 1 e 5 1e5 1e5 级别的。
用 f f f 数组实现并查集, n w nw nw 数组记录代表元素具体等于什么数, m p mp mp 数组表示某个数的位置集合的代表元素是什么。之后就是愉快的coding了一点都不愉快。
code1:
#include <iostream>
#include <cstdio>
#include <map>
#include <vector>
#define donothing 0
using namespace std;
const int maxn=1e5+5,maxf=1e6+5;int T,n,m,a[maxn];
int f[maxn],nw[maxn],mp[maxf];
int findf(int x){if(f[x]==x)return x;else return f[x]=findf(f[x]);
}
void merge(int a,int b){//把a合并到b上 int fa=findf(a),fb=findf(b);f[fa]=fb;
}int main(){cin>>T;while(T--){cin>>n>>m;for(int i=1;i<=n;i++)f[i]=i;
// for(int i=1;i<=1e6;i++)mp[i]=0;for(int i=1;i<=n;i++){cin>>a[i];nw[i]=a[i];if(!mp[a[i]])mp[a[i]]=i;else merge(i,mp[a[i]]);}for(int i=1,x,y;i<=m;i++){cin>>x>>y;if(!mp[x] || x==y)donothing;else if(!mp[y]){nw[mp[x]]=y;mp[y]=mp[x];mp[x]=0;}else {merge(mp[x],mp[y]);mp[x]=0;}}for(int i=1;i<=n;i++)cout<<nw[findf(i)]<<" \n"[i==n];for(int i=1;i<=n;i++)mp[nw[findf(i)]]=0;//直接memset会超时,只能这样清空}return 0;
}
思路2:
正解做法,正难则反。根据上面说的那个把合并过程看成从叶子节点走到根节点的过程,既然集合的合并很难,那么不如从根节点向叶子节点走。因为我们只关心叶子节点最后走到根节点时,根节点上是什么值,那我们从根走到叶子的时候只需要告诉叶子,根等于什么就好了。
code2:
#include <iostream>
#include <cstdio>
#include <map>
#include <vector>
using namespace std;
const int maxn=1e5+5,maxf=1e6+5;int T,n,m,a[maxn];
pair<int,int> opt[maxn];int main(){cin>>T;while(T--){cin>>n>>m;for(int i=1;i<=n;i++)cin>>a[i];for(int i=1;i<=m;i++)cin>>opt[i].first>>opt[i].second;map<int,int> mp;for(int i=m;i>=1;i--){if(mp.find(opt[i].second)!=mp.end())mp[opt[i].first]=mp[opt[i].second];else mp[opt[i].first]=opt[i].second;}for(int i=1;i<=n;i++)cout<<((mp.find(a[i])==mp.end()?a[i]:mp[a[i]]))<<" \n"[i==n];}return 0;
}
思路3:
启发式合并,本质是个优化的暴力。我们每次集合合并的时候,都把小集合合并到大集合上,就能减少合并次数,而最多合并 m 2 + m 4 + m 8 + ⋯ + m m ≥ m 1 + m 2 + m 3 + ⋯ + m m ≈ m l o g m \dfrac m2+\dfrac m4+\dfrac m8+\dots+\dfrac mm\ge \dfrac m1+\dfrac m2+\dfrac m3+\dots+\dfrac mm\approx mlogm 2m+4m+8m+⋯+mm≥1m+2m+3m+⋯+mm≈mlogm 次元素,可以通过。
code3:
#include <iostream>
#include <cstdio>
#include <map>
#include <vector>
using namespace std;
const int maxn=1e5+5,maxf=1e6+5;int T,n,m,a[maxn];
vector<int> f[maxf];int main(){ios::sync_with_stdio(false);cin.tie(0);cin>>T;while(T--){cin>>n>>m;vector<int> opt;for(int i=1;i<=n;i++){cin>>a[i];f[a[i]].push_back(i);opt.push_back(a[i]);}for(int i=1,x,y;i<=m;i++){cin>>x>>y;if(x==y)continue;if(f[x].size()<f[y].size()){f[y].insert(f[y].end(),f[x].begin(),f[x].end());f[x].clear();}else {f[x].insert(f[x].end(),f[y].begin(),f[y].end());f[y].clear();swap(f[x],f[y]);//只交换指针,O(1)}opt.push_back(y);}for(auto i:opt){for(auto idx:f[i])a[idx]=i;f[i].clear();}for(int i=1;i<=n;i++)cout<<a[i]<<" \n"[i==n];}return 0;
}
F 现在是消消乐时间
思路1:
这题非常色情。先说暴力思路,题解一两句话其实说的很明白了。对位置 ( d , 1 ) (d,1) (d,1) 这个砖块(也就是左下角那个砖块),我们消除它的方法只有两种,一种是主对角线,一种是副对角线,也就是下图中红线描红的那两条。
你无论从哪里开始走,往哪走,路径上一定会包含这么一小段对角线,那么我们不如直接从这里开始走。方向无所谓,所以可以只判断一下从 ( d , 0 ) (d,0) (d,0) 向右上以及 ( d , 1 ) (d,1) (d,1) 向左上两种情况是否可行即可。如果不可行,那么就一定无解,因为至少这块砖无法被消除。
实际上,在其他位置进行判断也是可行的,证明看出题人题解
code1:
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int maxn=2005;int n,m,d;
bool mp[maxn][maxn],vis[maxn][maxn][4];
int fx[]={1,1,-1,-1},fy[]={-1,1,-1,1};struct state{int x,y;int dir;state(int x,int y,int dir):x(x),y(y),dir(dir){};
};
queue<state> q;bool solve(int a,int b,int st){memset(mp,0,sizeof(mp));memset(vis,0,sizeof(vis));q.push(state(a,b,st));while(!q.empty()){int ux=q.front().x,uy=q.front().y,dir=q.front().dir,nw=dir;q.pop();
// printf("%d %d %d\n",ux,uy,dir);if(ux==0 && uy==m && dir!=0)break;if(ux==0 && uy==0 && dir!=1)break;if(ux==n && uy==m && dir!=2)break;if(ux==n && uy==0 && dir!=3)break;if((ux==0 && (dir==2 || dir==3)) || (ux==n && (dir==0 || dir==1)))nw=dir^2;if((uy==0 && (dir==0 || dir==2)) || (uy==m && (dir==1 || dir==3)))nw=dir^1;int x=ux+fx[nw],y=uy+fy[nw];mp[(ux+x+1)/2][(uy+y+1)/2]=true;
// printf("%d %d\n",(ux+x+1)/2,(uy+y+1)/2);if(!vis[x][y][nw]){q.push(state(x,y,nw));vis[x][y][nw]=true;}}for(int i=d+1;i<=n;i++)for(int j=1;j<=m;j++)if(!mp[i][j])return false;return true;
}int main(){cin>>n>>m>>d;if(solve(0,0,1))printf("YES\n%d %d\nUL\n",0,0);else if(solve(0,1,0))printf("YES\n%d %d\nUR\n",0,1);else puts("NO");return 0;
}
思路2:
结论:
- 如果 d = n d=n d=n,那么显然一定有解。
- 如果 g c d ( n , m ) = 1 gcd(n,m)=1 gcd(n,m)=1,那么在 ( 0 , 0 ) (0,0) (0,0) 向右上
- 如果 g c d ( n , m ) = 2 gcd(n,m)=2 gcd(n,m)=2,那么在 ( 1 , 0 ) (1,0) (1,0) 向左上
- 其他情况无解。
证明,我是理解不能
code2:
某位大佬的
G 三点不共线
思路1:
赛时丑陋的想法,根据高中解三角形的知识来解。直觉上感觉这个 C C C 点应该离 A B AB AB 边越远,夹角就越小,反之则越大,而在所有点中,在中垂线上的点的性质最为美妙,因为它是对称的。 C C C 点在 A B AB AB 上的中点沿着中垂线开始向外走,走的越远,形成的夹角就越小。
考虑用数学语言表示每个值。要模拟 C C C 点在 A B AB AB 上的中点沿着中垂线向外走,就需要知道中垂线上的点的坐标,而要知道线上点的坐标,就需要知道xy值的变化率,直线 A B AB AB 与中垂线垂直,那么就可以通过 A B AB AB 来算这个变化率。设 l e n = ∣ A B ∣ len=|AB| len=∣AB∣ 。通过画图可以比较容易算出来,如下图:
然后中点坐标 ( x 1 + x 2 2 , y 1 + y 2 2 ) (\dfrac{x_1+x_2}2,\dfrac{y_1+y_2}2) (2x1+x2,2y1+y2),加上移动距离 d i s dis dis 乘上单位向量 e → \overrightarrow e e 就能得到 中垂线上 到中点距离为 d i s dis dis 的点的坐标了。
所以问题变成了怎么求得这个距离 d i s dis dis。因为形成的夹角为 α \alpha α 时,腰的长度应该是固定的,当 d i s dis dis 变大的时候,夹角 α \alpha α 就会变小,三角形的腰就会变长。因此可以二分答案。当腰长小于答案腰长时,就增大 d i s dis dis,否则就减少。
答案腰长可以根据余弦定理求得,假设腰长为 x x x,有: x 2 + x 2 − l e n 2 = 2 ∗ x ∗ x ∗ c o s α x^2+x^2-len^2=2*x*x*cos\,\alpha x2+x2−len2=2∗x∗x∗cosα 2 ∗ x 2 ∗ ( 1 − c o s α ) = l e n 2 2*x^2*(1-cos\,\alpha)=len^2 2∗x2∗(1−cosα)=len2 x 2 = l e n 2 2 ∗ ( 1 − c o s α ) x^2=\dfrac{len^2}{2*(1-cos\,\alpha)} x2=2∗(1−cosα)len2
而当 d i s dis dis 取得某个值的时候根据勾股定理可以得到此时的腰长: x 2 = l e n 2 4 + d i s 2 x^2=\dfrac{len^2}4+dis^2 x2=4len2+dis2
需要用到的 π \pi π 可以通过 4 ∗ a r c t a n ( 1 ) 4*arctan(1) 4∗arctan(1) 来计算(因为 t a n π 4 = 1 tan\,\dfrac{\pi}{4}=1 tan4π=1)。
code1:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const double eps=1e-7;int T;
double x11,y11,x22,y22,alpha,pi;double dis(double a1,double b1,double a2,double b2){return sqrt((a1-a2)*(a1-a2)+(b1-b2)*(b1-b2));
}
double angle(double a0,double b0,double a1,double b1,double a2,double b2){double a=dis(a0,b0,a1,b1),b=dis(a0,b0,a2,b2),c=dis(a1,b1,a2,b2);return acos((a*a+b*b-c*c)/(2*a*b));
}int main(){pi=4*atan(1);cin>>T;while(T--){cin>>x11>>y11>>x22>>y22>>alpha;alpha=alpha*pi/180;double x0=(x11+x22)/2,y0=(y11+y22)/2;double len2=(x11-x22)*(x11-x22)+(y11-y22)*(y11-y22),len=sqrt(len2);double kx=(y11-y22)/len,ky=(x22-x11)/len;
// printf("***(%lf,%lf) %lf %lf\n",x0,y0,kx,ky);double l=0,r=1e9,mid;while(l+eps<r){mid=(l+r)/2;if(len2/4+mid*mid<len2/(2*(1-cos(alpha))))l=mid;else r=mid;}double ans=(l+r)/2;printf("%.9lf %.9lf\n",x0+kx*ans,y0+ky*ans);
// printf("***%lf\n",angle(x0+kx*ans,y0+ky*ans,x11,y11,x22,y22)*180/pi);}return 0;
}
思路2:
我们把 A B → \overrightarrow {AB} AB 旋转到 B C → \overrightarrow {BC} BC 的位置上,然后算出腰长,然后就可以从 B B B 点直接算出 C C C 点了。
旋转的角度从图上很容易算出来等于 π 2 − α 2 \dfrac{\pi} 2-\dfrac{\alpha} 2 2π−2α,腰长根据正弦公式可以很容易算出来: x s i n π 2 = l e n 2 s i n α 2 \dfrac x{sin\,\dfrac{\pi}{2}}=\dfrac {\dfrac{len}{2}}{sin\,\dfrac{\alpha}{2}} sin2πx=sin2α2len x = l e n 2 ∗ s i n α 2 x=\dfrac {len}{2*sin\,\dfrac{\alpha}{2}} x=2∗sin2αlen
关于求逆时针旋转 α \alpha α 度的旋转矩阵的方法:
使用待定系数法,假设这个旋转矩阵为 [ a b c d ] \left[\begin{matrix} a & b\\ c & d \end{matrix}\right] [acbd]。先画一个单位圆,我们可以把向量 ( 1 , 0 ) (1,0) (1,0) 逆时针旋转 α \alpha α 到 ( c o s α , s i n α ) (cos\,\alpha,sin\,\alpha) (cosα,sinα),于是有: [ a b c d ] × [ 1 0 ] = [ c o s α s i n α ] \left[\begin{matrix}a & b\\c & d\end{matrix}\right]\times\left[\begin{matrix}1\\0\end{matrix}\right]=\left[\begin{matrix}cos\,\alpha\\sin\,\alpha\end{matrix}\right] [acbd]×[10]=[cosαsinα] { a = c o s α c = s i n α \left\{\begin{aligned} a & = cos\,\alpha\\ c & = sin\,\alpha \end{aligned}\right. {ac=cosα=sinα
同理,把向量 ( 0 , 1 ) (0,1) (0,1) 逆时针旋转 α \alpha α 到 ( c o s α , s i n α ) (cos\,\alpha,sin\,\alpha) (cosα,sinα) 同时相当于把向量 ( 1 , 0 ) (1,0) (1,0) 旋转到 ( c o s ( α + π 2 ) , s i n ( α + π 2 ) ) = ( − s i n α , c o s α ) (cos\,(\alpha+\dfrac\pi2),sin\,(\alpha+\dfrac\pi2))=(-sin\,\alpha,cos\,\alpha) (cos(α+2π),sin(α+2π))=(−sinα,cosα) ,那么有: [ a b c d ] × [ 0 1 ] = [ − s i n α c o s α ] \left[\begin{matrix}a & b\\c & d\end{matrix}\right]\times\left[\begin{matrix}0\\1\end{matrix}\right]=\left[\begin{matrix}-sin\,\alpha\\cos\,\alpha\end{matrix}\right] [acbd]×[01]=[−sinαcosα] { b = − s i n α d = c o s α \left\{\begin{aligned} b & = -sin\,\alpha\\ d & = cos\,\alpha \end{aligned}\right. {bd=−sinα=cosα
所以这个旋转矩阵就是 [ c o s α − s i n α s i n α c o s α ] \left[\begin{matrix} cos\,\alpha & -sin\,\alpha\\ sin\,\alpha & cos\,\alpha \end{matrix}\right] [cosαsinα−sinαcosα]。
code:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;int T;
double a1,b1,a2,b2,alpha,pi;int main(){pi=4*atan(1);cin>>T;while(T--){cin>>a1>>b1>>a2>>b2>>alpha;alpha=alpha*pi/180;double peta=(pi-alpha)/2;pair<double,double> AB=make_pair(a2-a1,b2-b1);pair<double,double> AC=make_pair(AB.first*cos(peta)-AB.second*sin(peta),AB.first*sin(peta)+AB.second*cos(peta));
// printf("***%.9lf %.9lf\n",AC.first,AC.second);double ABl=sqrt(AB.first*AB.first+AB.second*AB.second);double ACl=ABl/(2*sin(alpha/2));AC.first=AC.first/ABl*ACl;AC.second=AC.second/ABl*ACl;printf("%.9lf %.9lf\n",a1+AC.first,b1+AC.second);}return 0;
}