hh集训营 练习回顾
-
收录一些在hh做的题。
-
原题很多找不到。在这里征求一下。
树论(六)
题意概述
给定一棵树,每个点有一个点权。小A先从树上选取一个点集使得集合里的任意两个点之间没有边直接相连,小B则获得剩下的点,问是否存在方案使得小A选取的节点的异或和严格大于小B的。
solution
考场首 A 。不明白为什么那么多人写了一个很何意味的树形DP,本质题目水放过去的。警示我们一旦搓出来一个很像正解但是看起来不像那么回事的东西还是要多验证一下。
注意到异或满足交换律和结合律。并且按照异或的性质,假如说 $ x \oplus y $ 的结果为 $ 0 $ 那么一定有 $ x = y $ 。
所以假如有一棵树,这个树所有点权的异或和为 $ 0 $ ,那么小A不论怎么选一定都无法严格大于小B。因为在树上选取一个点集相当于在把整个树所有的点权异或起来的式子里提取出小A选取的部分(交换律)并异或到一起(结合律),此时把剩下小B的部分也异或起来然后和小A选取的部分的异或和异或,此时结果不变仍然是0,所以这个时候小A的部分一定等于小B的部分。
反之必定存在小A大于小B。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100005;
int n,a[N],sum;
vector<vector<int> >g;
signed main(){
freopen("tree6.in","r",stdin);
freopen("tree6.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],sum^=a[i];
/*g.resize(n+1);
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}*/
if(sum==0)cout<<"No";
else cout<<"Yes";
return 0;
}
表达式
题意概述
给定一个数组 $ a $ ,在 $ a $ 的元素之间插入一些四则运算符,然后我们会得到一些表达式,求这些表达式的结果的数学期望。结果对 998244353 取模。
solution
其实挺常规的吧。明明都想到了结果复杂化了。要是正常做数学推导就场切了。还是概率/期望之类的东西做少了。
参考了Youshinow的做法。
注意到 $ + $ 和 $ - $ 的期望最后是可以互相抵消的。但是乘除则需要考虑最后一段的情况。
于是我们考虑维护两个东东:$ tot $ ,和 $ lst $ ,分别表示前 $ i $ 个表达式整体的期望,以及前 $ i $ 个表达式中,最后一段的期望是多少。
我们不断往表达式里加入数组 $ a $ 的值。假定我们往式子后面放了一个 $ x $ ,那么:
- 填入 $ +x $ ,那么整体期望加 $ x $ ,最后一段的期望变为 $ x $ 。
- 填入 $ -x $ ,那么整体期望减 $ x $ ,最后一段的期望变为 $ -x $ 。
- 填入 $ \times x $ ,最后一段的期望变为 $ lst \times x $ ,整体期望则要先减去旧的 $ lst $ 再加上新的 $ lst $ 。
- 填入 $ \div x $ ,最后一段的期望变为 $ lst \times x^{-1} $ ,整体期望逻辑与上面相同。
因为四种运算符填入的概率相同,所以总答案要除以 4 。
写成公式,我们就可以得到:(此处数学公式渲染使用deepseek润色)
化简得
然后正常递推计算即可。注意除法要用费马小定理+快速幂转化成逆元。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int n,lst,tot,f,x;
inline int fp(int a,int b){
int res=1;
while(b){
if(b%2)res*=a,res%=mod;
a*=a;a%=mod;
b/=2;
}
return res;
}
signed main(){
freopen("expression.in","r",stdin);
freopen("expression.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>x;
f=fp(4,mod-2);
lst=tot=x;
for(int i=2,rx;i<=n;i++){
cin>>x;
rx=fp(x,mod-2);
tot=(tot+lst*f%mod*(x-2+rx)%mod)%mod;
lst=lst*f%mod*(x+rx)%mod;
}
cout<<tot;
return 0;
}
XOR with GCD
题意概述
给定一个 $ n $ ,求 \(\bigoplus\limits_{i=1}^{n} \gcd(i,n)\),即 \(\gcd(1,n) \oplus \gcd(2,n) \oplus \cdots \oplus \gcd(n,n)\) 的值。
solution
妙妙脑筋急转弯题。考场大体思路其实没问题。思考的再深入一点就说不定可以场切了。
注意到 $ a \oplus b \oplus b $ 等于 $ a $ 。这个时候我们就可以往构造相同上想。
注意到 $ gcd(i,n) $ 等于 $ gcd(n-i,n) $ 。这个其实就是更相减损术的基本原理。
证明也不难。假定有 $ k = gcd(i,n) $ 并且 $ k < gcd(n-i,n) $ ,这个时候 $ n \bmod k = i \bmod k = 0 $ 。所以一定也 $ ( n - i ) \bmod k = 0 $ 。这个时候假设就不成立了。
所以前后的 gcd 其实是相互抵消的。所以我们可以将 \(1\) 到 \(n-1\) 中的 \(i\) 和 \(n-i\) 进行两两配对。接下来根据 \(n\) 的奇偶性分情况讨论:
-
当 \(n\) 为奇数时:
对于任意 \(1 \le i \le n-1\),\(i\) 和 \(n-i\) 必然不相等(因为 \(n\) 是奇数,不存在 \(i = n-i\) 的整数解)。因此,\(1\) 到 \(n-1\) 的所有项可以完美地两两配对,每对的 \(\gcd\) 值相等,在异或和中全部抵消为 \(0\)。
最后只剩下 \(i = n\) 时的项,即 \(\gcd(n, n) = n\)。
所以,当 \(n\) 为奇数时,答案为 \(n\)。 -
当 \(n\) 为偶数时:
同样地,我们可以将 \(i\) 和 \(n-i\) 配对。但是,当 \(i = \frac{n}{2}\) 时,\(n-i = \frac{n}{2}\),此时它自己和自己“配对”,无法与其他不同的项抵消。
除了 \(i = \frac{n}{2}\) 之外,其余 \(1\) 到 \(n-1\) 的项依然可以两两配对并抵消为 \(0\)。
因此,\(1\) 到 \(n-1\) 的异或和中,唯一剩下的项是 \(i = \frac{n}{2}\) 时的 \(\gcd(\frac{n}{2}, n) = \frac{n}{2}\)。
最后再加上 \(i = n\) 时的项 \(\gcd(n, n) = n\)。
所以,当 \(n\) 为偶数时,答案为 \(\frac{n}{2} \oplus n\)。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100000001;
int t;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>t;
while(t--){
int n;
cin>>n;
if(n%2)cout<<n;
else cout<<(n^(n/2));
cout<<'\n';
}
return 0;
}
Yoshinow and 01 Game
没有原题。
题意概述
Yoshinow 在和 Stump 玩一个游戏。具体的,给定一个01串,每次可以将一个数变成它前面的数或者变成它后面的数,最后谁不能操作了谁就输。假如两个人智商都够高,如果能赢都会赢,不能赢都会尽量让自己不败,并且 Yoshinow 先操作,问最后的结果是 Yoshinow / Stump 赢还是无法结束?
solution
瞎猜出来的结论一定要验一下。不然吃一堆罚时就好玩了。另外博弈论的题其实可以从两个人的操作出发思考。
显然的每次操作就是把一对不同的 0/1 变成相同的,所以一定会让 0/1 的个数的奇偶性发生改变。
那么直观上,只要 0/1 个数比较多,总会陷入游戏无法结束的局面。
分类讨论几种不同情况。
-
串内只有 1/0 。显然的此时 Stump 赢。
-
串内只有一个 1/0 。显然的此时 Yoshinow 赢。
-
串长度为 4 。如果串长度为 4 并且不符合上面的两种情况,说明一定有两个 0 和两个 1 。此时无论先手怎样操作都会变成第二种情况,所以后手必胜。
-
串长度大于 4 。此时我们假设先手有必胜策略,先手通过一次操作把一个 01 变成 11(或 00),后手为了不失败,总是可以找到一个位置,使得 1 的个数变少(或变多),此时游戏无法结束。假定后手有必胜策略同理。
然后就没了。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1005;
int t,a[N],n;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>t;
a[0]=-1;
while(t--){
cin>>n;
a[n+1]=-1;
int cnt=0;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i])cnt++;
}
if(cnt==0||n-cnt==0||cnt==2&&n==4)cout<<"Stump\n";
else if(cnt==1||n-cnt==1)cout<<"Yoshinow\n";
else cout<<"=A=\n";
a[n+1]=0;
}
return 0;
}
Kuroni and Simple Strings
题意概述
定义简单字符串的组成部分为一个长度为偶数的回文串,且左边只有左括号右边只有右括号。
给定一个只由括号组成的字符串,每次操作可以在这个字符串里提取出一个简单字符串并删除,问最少要删除几次使得无法再删除?
solution
贪心题一定要认真思考自己的贪心策略是否正确,必要的时候可以简单证明,宁可慢点切不要 WA 。
贪心。直接维护一对双指针,左指针维护选到的左括号,右指针维护选到的右括号,从左右往中间收缩。每次直接选取匹配到的连续段,然后再用一个 while 跳过无法选取的部分即可。
显然只需要提取一次就可以提掉所有可以匹配的部分。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353,N=1005;
string s;
int cnt;
vector<int>a;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>s;
int r=s.size()-1,l=0;
while(l<r&&l<s.size()&&r>=0){
while(s[l]==')'&&l<s.size())l++;
while(s[r]=='('&&r>=0)r--;
if(r<0||l>=s.size()||l>r)break;
a.push_back(l+1);
a.push_back(r+1);
l++;
r--;
}
sort(a.begin(),a.end());
if(a.size()==0){
cout<<0;
exit(0);
}
cout<<"1\n"<<a.size()<<'\n';
for(int i=0;i<a.size();i++)cout<<a[i]<<' ';
return 0;
}
Kuroni and Impossible Calculation
题意概述
给定一个数组和一个数,求每个数组元素两两之间差的乘积对这个数取模的结果。
1 <= 这个数 <= 1000
solution
还是那句话,注意数据范围 + 从操作入手。
脑筋急转弯题。
每个数组元素显然可以先对模数取模。反正这样不影响计算结果。
常规计算没有什么好的优化方法。观察题目,注意到这里的模数小且会改变。我们可以从取模入手。
因为结果是乘积,所以假如有两个元素取模后的结果相同,那么结果一定是 $ 0 $ 。
还是那句话,注意到这里的模数小且会改变。这个时候就可以联想到假如 $ n > m $ ,那么结果一定是 $ 0 $ 。因为我们给 $ a_1 $ 到 $ a_m $ 分别分配了 $ 0 $ 到 $ m - 1 $ 的余数,那么剩下 $ a_{m+1} $ 到 $ a_n $ 的部分一定会和之前的重合(其实就是一个鸽巢原理)。
如果 $ n < m $ 呢?注意到 $ m $ 很小,直接暴力即可。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200005;
int mod,n,a[N],s[N];
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>mod;
for(int i=1;i<=n;i++)cin>>a[i];
if(n>mod)cout<<0;
else{
int ans=1;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
ans*=abs(a[j]-a[i]);
ans%=mod;
}
}
cout<<ans;
}
return 0;
}
Erase First or Second Letter
题意概述
给定一个字符串,每次可以删除这个字符串的第一个字符和第二个字符,问最后可能产生多少个不一样的字符串?
solution
场掉了,但是前面浪费了很多时间。这种简单的操作题最好是模拟一下操作,找出一些规律然后做一些简单的证明。
做几次这样的操作,相当于选择一个后缀,再在前面选择一个字符组装起一个字符串。
每个后缀长度不相同所以不会重合。只需要用一个布尔数组和一个长度变量维护前面有多少个字符选择过/没有选择过就可以了。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100005;
int t,n;
string s;
bool vis[N];
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>t;
while(t--){
cin>>n>>s;
int cnt=0,res=0;
memset(vis,0,sizeof vis);
for(int i=0;i<s.size();i++){
int hs=s[i]-'a';
if(!vis[hs]){
vis[hs]=1;
cnt++;
}
res+=cnt;
}
cout<<res<<'\n';
}
return 0;
}
奶酪
题意概述
在一个三维空间内有一个 \(x\) 和 \(z\) 无限大,具有一定高度的奶酪,其内部有一些空洞,空洞为一些半径相同的球体,问可不可以从这个奶酪的下表面通过空洞到达上表面。
soluton
赛后瞪了 1s 直接秒了。没办法 T2 条太久了。
瞪眼思路:
-
看起来就很像一个类似图论建模的东西。
-
假如两个空洞 相切 / 相交 ,那么我们可以把这两个空洞连一条无向边。
-
令奶酪下表面为 $ 0 $ ,上表面为 $ n + 1 $ ,那么我们就可以把和下表面和上表面用和空洞同样的逻辑处理。(相交 / 相切就连边)。
-
最后判断 $ 0 $ 和 $ n + 1 $ 是否连通即可。
-
显然这个东西用并查集是非常好维护的。
另外为了避免精度问题,我们可以把 dist 函数返回的结果先平方再比较。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1005;
int n,fa[N],h,r;
struct pt{
int x;
int y;
int z;
}a[N];
inline int dis(pt x,pt y){
return (x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y)+(x.z-y.z)*(x.z-y.z);
}
inline int find(int x){if(fa[x]==x)return x;return fa[x]=find(fa[x]);}
inline void merge(int x,int y){fa[find(x)]=find(y);}
inline void sol(void){
cin>>n>>h>>r;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y>>a[i].z;
fa[i]=i;
}
fa[n+1]=n+1;
fa[0]=0;
for(int i=1;i<=n;i++){
if(a[i].z<=r&&find(i)!=find(0))merge(i,0);
if(a[i].z+r>=h&&find(i)!=find(n+1))merge(i,n+1);
for(int j=1;j<i;j++){
if(find(i)==find(j))continue;
if(dis(a[i],a[j])<=4*r*r)merge(i,j);
}
}
if(find(0)==find(n+1))cout<<"Yes\n";
else cout<<"No\n";
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int T;
cin>>T;
while(T--)sol();
return 0;
}
交换位置
原题被猫猫吃掉了。
题意概述
给定一个序列 \(A\) 和一个目标序列 \(B\),序列中的每个元素互不相同,每次操作可以选定一组 $ i,j $,满足 \(j−i=k\) 且 \(k\) 为正整数,交换 \(a_i\),\(a_j\)。求所有可以使得原序列成功变成目标序列的 \(k\) 。
solution
一些P话
这真的是 J 组的 T2 吗。
什么叫我的切题顺序是1->4->3->2。
哦对了这篇题解比较石有不懂的可以在回复问我。
如果一个元素可以通过间隔为 $ k $ 的交换操作移到其在目标数组的相应位置,那么这个元素在原数组的下标和在目标数组的下标对 $ k $ 取模的值一定是相同的。
我对上面那个结论的理解
(我的考场想法,比较复杂,仅供参考):- 原操作相当于把所有下标对 $ k $ 取模的值相同的值划进同一组。
- 如果一个组在原序列的元素和在目标序列的元素在不关注顺序的情况下不完全相同,则一定无法完成。
- 那么一个元素要在两边都被划分到同一组,就需要这个元素在原数组的下标和在目标数组的下标对 $ k $ 取模的值相同。
既然这个元素在原数组的下标和在目标数组的下标对 $ k $ 取模的值一定是相同的,那么这个元素在原数组的下标和在目标数组的下标的差的绝对值对 $ k $ 取模一定为 $ 0 $ 。那么如果一个 $ k $ 是可以让序列成功变换的,这个 $ k $ 一定是所有对应下标与原下表的差的绝对值的公因数。
所以我们就可以求出所有对应下标,然后算出所有的差,然后求出所有的差的最大公因数即可。所有的公因数一定都是所有的差的最大公因数的因数。
现在还有一个问题是对应下标怎么求。暴力 $ O(n^2) $ 找显然是会爆炸的。注意到数据范围是 2e5 ,考虑 $ O(\log n) $ 做法。
很容易想到二分吧。用一个结构体打包这个数的原值和原下标(因为排序之后下标改变),然后在目标序列的原值序列上做二分就好了。
最后求个 gcd ,再把 gcd 里的因数求出来就可以了。
总的时间复杂度是 $ O(n \log n)$ 。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200005;
int n,a[N];
struct f{
int val;
int pos;
}b[N];
bool cmp(f x,f y){
return x.val<y.val;
}
vector<int>len,ans;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
cin>>b[i].val;
b[i].pos=i;
}
sort(b+1,b+1+n,cmp);
int l,r,mid;
for(int i=1;i<=n;i++){
l=0;
r=n+1;
while(l+1<r){
mid=l+(r-l)/2;
if(b[mid].val>a[i])r=mid;
else l=mid;
}
len.push_back(abs(b[l].pos-i));
}
int g=len[0];
for(int v:len)g=__gcd(g,v);
int sq=sqrt(g);
for(int i=1;i<=sq;i++){
if(i*i==g)ans.push_back(i);
else if(g%i==0)ans.push_back(i),ans.push_back(g/i);
}
sort(ans.begin(),ans.end());
for(int v:ans)cout<<v<<'\n';
return 0;
}
Knight
题意概述
国际象棋里的一个骑士想要从一个平面直角坐标系的原点出发到达 $ ( x , y ) $ 点。问一共有多少种方案。
solution
为了方便表示,我们令每个 $ x + 2 $ 的移动为向下移动,另一个移动则是向右移动。
注意到 $ x \le 1000000 $ 且 $ y \le 1000000 $ ,那常规递推肯定是做不了的。
看起来常规递推没办法优化到线性时间复杂度,并且 $ x $ 和 $ y $ 都很大,应该是个类似于数学的东西,于是考虑数学。
首先容易观察到可以走到的 $ x $ 和 $ y $ 加起来的和一定是 $ 3 $ 的倍数。因为骑士每次走一步的 $ x $ 和 $ y $ 增量的总和一定是 $ 3 $。
所以如果给出的 $ x $ 和 $ y $ 加起来的和不是 $ 3 $ 的倍数,那么这个 $ x $ 和 $ y $ 就不合法。
然后根据上面 “ 骑士每次走一步的 $ x $ 和 $ y $ 增量的总和一定是 $ 3 $ ” 的结论 ,我们可以得到总的移动步数一定是$ ( x + y ) / 3 $。
而构造一个移动方案相当于从每一次移动里选出一些并令其 向右/向下 。容易猜到 向右/向下 的步数一定是固定的,具体是什么可以推一下。
令这个总的移动步数为 $ len $ (起的啥名字啊),向右移动的步数为 $ m $ ,则有:
化简可得
最后计算 $ C_{len}^{m} $ 即可。
AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1000005,mod=1e9+7;
int n,m,len,fac[N],inv[N];
inline int fp(int a,int b){
int res=1;
while(b){
if(b&1)res*=a,res%=mod;
a*=a;a%=mod;
b/=2;
}
return res;
}
inline int calc(int n,int m){//n选m
if(n<0||m<0||n<m)return 0;
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;//相当于x和y
if((n+m)%3){
cout<<0;
exit(0);
}
len=(n+m)/3;
fac[0]=inv[0]=inv[1]=fac[1]=1;
for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%mod;
inv[n]=fp(fac[n],mod-2);
for(int i=n-1;i>=2;i--)inv[i]=inv[i+1]*(i+1)%mod;
cout<<calc(len,2*len-n);
return 0;
}

浙公网安备 33010602011771号