NFLS 训练总结 2
前言
接上周。
Day 6
总体情况
1000+1200+1400+1700+1800+1408+0+300=8808,rk83
大寄,为什么他们分都那么高啊!
T1
从 T1 就开始卡。
简单的贪心,买票最少就是每个大人都尽量带孩子,而最多就是所有孩子都由一个大人带。
如果没有大人只有孩子,就是 “Impossible”。
然后有人 Impossible 的 I 没有大写,被 WA 懵了。
T2
勉强做的还算顺的一道题。
首先四个人的一辆车,然后三个人的尽量和一个人的匹配,配不完只能单独,两个人的两两匹配,若有剩余则继续和一个人的匹配。
T3
这题就比较简单了。
按左端点排序,一个一个合并,若新加的区间与之前的无交,就算上之前合并的贡献,然后继续合并,最后加上最后一段区间的贡献。
sort(a+1,a+n+1);
l=a[1].s,r=a[1].t;
for(int i=2;i<=n;++i){
if(a[i].s<=r) r=max(r,a[i].t);
else{
ans+=(r-l+1); l=a[i].s,r=a[i].t;
}
}
ans+=(r-l+1);
T4
卡在这题上了,之前一直想了一个错误的贪心思路,想了半天。
后来才发现可以 dp。
设 \(dp[i][0/1]\) 表示将 \(i\) 位之前的数都变成 \(A/B\) 的最小代价,转移即可。
#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;
int n,l,r,ans=0,dp[maxn][2]; char s[maxn];
int main(){
cin>>n;
for(int i=1;i<=n;++i) cin>>s[i];
memset(dp,0x3f,sizeof(dp));
dp[0][0]=dp[0][1]=0;
for(int i=1;i<=n;++i){
if(s[i]=='A'){
dp[i][0]=min(dp[i-1][0],dp[i-1][1]+2);
dp[i][1]=min(dp[i-1][1]+1,dp[i-1][0]+1);
}
else{
dp[i][1]=min(dp[i-1][1],dp[i-1][0]+2);
dp[i][0]=min(dp[i-1][0]+1,dp[i-1][1]+1);
}
}
printf("%d\n",dp[n][0]);
return 0;
}
T5
有一点用到 prim 的思想,每个点以最小的代价接到树里面,但是用 kruskal 实现。
可以发现要让每个点接到树里面的边尽量小,所以每个点与和自己某个坐标最近的点连边,一定比与此坐标离它更远的点连这个坐标的距离,要更优。(感性理解
并且这样可以保证连通。
所以将三个坐标分别排序,然后相邻的点连权值为这个坐标的差的边,一共 \(3\cdot(n-1)\) 条边,用 kruskal 跑最小生成树。
#include<bits/stdc++.h>
#define maxn 100010
#define int long long
using namespace std;
struct planet{int x,y,z,id;}a[maxn];
struct edge{int u,v,w;}eg[maxn*10];
bool operator <(edge a,edge b){return a.w<b.w;}
bool cmp1(planet a,planet b){return a.x<b.x;}
bool cmp2(planet a,planet b){return a.y<b.y;}
bool cmp3(planet a,planet b){return a.z<b.z;}
int n,m,x,y,f[maxn],cnt=0,ans=0;
int find(int x){
if(x!=f[x]) return f[x]=find(f[x]);
return x;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld%lld%lld",&a[i].x,&a[i].y,&a[i].z);
a[i].id=i; f[i]=i;
}
sort(a+1,a+n+1,cmp1);
for(int i=2;i<=n;++i)
eg[++m]=(edge){a[i-1].id,a[i].id,a[i].x-a[i-1].x};
sort(a+1,a+n+1,cmp2);
for(int i=2;i<=n;++i)
eg[++m]=(edge){a[i-1].id,a[i].id,a[i].y-a[i-1].y};
sort(a+1,a+n+1,cmp3);
for(int i=2;i<=n;++i)
eg[++m]=(edge){a[i-1].id,a[i].id,a[i].z-a[i-1].z};
sort(eg+1,eg+m+1);
for(int i=1;i<=m;++i){
x=find(eg[i].u),y=find(eg[i].v);
if(x==y) continue;
ans+=eg[i].w; f[x]=y;
++cnt; if(cnt==n-1) break;
}
printf("%lld\n",ans);
return 0;
}
T6
把原序列翻转,将问题转化为求原序列与翻转序列 LCIS。
求 LCIS 的时候用一些处理,就是不是同一个数的 \(+2\),其余 \(+1\),求出来的就是此题要的最长回文的,先上升再下降的,子序列。
设 \(dp_{i,j}\) 表示当前考虑第一个序列 \(i\) 个,以第二个序列 \(j\) 结尾的最长公共上升子序列的长度(此题特殊的 \(\times 2\))
核心代码如下(\(a\) 是原序列,\(b\) 是翻转后的序列):
for(int i=1;i<=n;++i){
int mx=0;
for(int j=1;j<=n-i+1;++j){
if(a[i]==b[j]) dp[j]=max(dp[j],mx+1+(j!=n-i+1));
else if(a[i]>b[j]) mx=max(mx,dp[j]);
ans=max(ans,dp[j]);
}
}
T7
来源:P5038
网络流,稍微有一点难的。
每次操作都是相邻的两个点,所以考虑对原图黑白染色,每次操作一定是一个黑点和一个白点。
设 \(sa,sb\) 表示所有黑点和白点的值的和,\(ca,cb\) 表示黑点和白点的个数。
设最终所有数都等于 \(x\)。
每次操作都是黑白点权值各加一,所以如果要操作成功,必须满足的条件是 \(ca\cdot x-sa=cb\cdot x-sb\)。
发现当 \(ca\neq cb\) 时,\(x\) 可以直接解出来。所以源点向黑点连权值为 \(x-a_{i,j}\) 的边,黑点和相邻的白点连权值为 \(\inf\) 的边,白点向汇点连权值为 \(x-a_{i,j}\) 的边,跑最大流。若最大流等于 \(ca\cdot x-sa\) 就可行。
当 \(ca=cb\) 时,由于白点黑点一样多,发现答案满足单调性,如果 \(x\) 可行,那么在用 \(\frac{n\cdot m}{2}\) 次操作每个点加一,所以 \(x+1\) 一定可行。二分 \(x\),与上面用同样的判断即可。
注意一些细节,染色的时候黑色是 \((i+j)\equiv 1 \pmod 2\) 而不是 \(id_{i,j}\equiv 1 \pmod 2\)。还有这题是长方形,别把 \(m\) 写成 \(n\)。
还有!Dinic 板子不能写错!!!
一些有趣的逝:
某一些无聊的同学(包括我)在 AC 了这道题之后,开始比谁的常数小,经过一些开小数组,适当减小二分上界等玄学方法,我卡出了不开 O2 5.80s,开 O2 2.34s 的好成绩。
然而 hsy 随便交一发,就 2.29s,被吊打的很彻底。果然人傻常数大。
然后发现他不开全局 \(long long\) ,一些能用位运算的用位运算解决。
准备改变代码习惯,但全局 \(long long\) 不准备改。
其他
开学了,分在 7 班,4楼。
刚准备高兴,突然意识到机房在 5 楼,跑得更远。(恼
不管了,反正上学让我少爬一层就行了。
然后这几天感觉用脑超负荷,回家直接先睡个 \(30min\)。
困。
Day 7
总体情况
1000+1200+1400+1600+1440+2100=8740,rk32
T1
扫一遍找最大再扫一遍输出就行。
T2
记录前缀后缀最大,每次取一个数之前最大和之后最大的较大值。
T3
从一头一尾开始扫,每次较小的一边合并,直到相等,一直重复这么做,直到左右指针重合。
不会证明,但感觉没有别的构造方法比这个合理。
T4
考虑何时要交换两个数字。
设两个相邻数字字符串为 \(s_i\) 和 \(s_j\) \((i<j)\),显然是 \(s_i+s_j\) 不如 \(s_j+s_i\) 小的时候才要交换。
然后这人输出的时候写了 i<=t.length()
导致多输出了一个不知道什么字符,然后 WA 0,甚至怀疑自己做法假了。
感觉有一点像 exchange argument。
#include<bits/stdc++.h>
#define maxn 10010
using namespace std;
int n,flg; string s[maxn],t;
bool cmp(string s,string t){
return (s+t)<(t+s);
}
int main(){
cin>>n; flg=0;
for(int i=1;i<=n;++i) cin>>s[i];
sort(s+1,s+n+1,cmp);
for(int i=1;i<=n;++i) t+=s[i];
for(int i=0;i<t.length();++i){
if(t[i]!='0') flg=1;
if(flg) cout<<t[i];
}
if(!flg) cout<<'0';
cout<<endl;
return 0;
}
T5
状态比较好想:设 \(dp_{i,j}\) 表示上一步在 \(j\),这一步到 \(i\) 的最大得分。
考场上写的填表法,\(O(n^3)\),感觉优化的话,刷表法更好优化一点,刷表法先枚举的是中间一维。
教练说一般来说要枚举三维的,先枚举中间一维比较方便优化。一般来说枚举中间推后面的是刷表。
考场上听说优先队列 \(O(n^2\log n)\) 被卡了,这个蒟蒻挑战人品,去卡优先队列的做法了,卡过了就补这题总结。
upd:今天人品不错,一遍就卡过去了。虽然最慢的点 47ms。
下面放的代码是算向后跳的核心代码(整体代码太长了),向前跳稍微改改就行了。
填表法(无优化 \(80pts\)):
memset(dp,0xcf,sizeof(dp));
for(int i=1;i<=n;++i){
dp[i][i]=a[i].val;
for(int j=1;j<i;++j){
for(int k=j;k>=1;--k){
if(a[i].x-a[j].x>=a[j].x-a[k].x)
dp[i][j]=max(dp[i][j],dp[j][k]+a[i].val);
else break;
}
ans=max(ans,dp[i][j]);
}
}
刷表法(优先队列优化 \(100pts\)):
memset(dp,0xcf,sizeof(dp));
for(int i=1;i<=n;++i) dp[i][i]=a[i].val;
for(int i=1;i<=n;++i){
int k=i;
while(q.size()) q.pop();
for(int j=i+1;j<=n;++j){
while(k>=1&&a[j].x-a[i].x>=a[i].x-a[k].x){
q.push(dp[i][k]); --k;
}
dp[j][i]=max(dp[j][i],q.top()+a[j].val);
ans=max(ans,dp[j][i]);
}
}
刷表法(直接单调性优化 \(100pts\)):
memset(dp,0xcf,sizeof(dp));
for(int i=1;i<=n;++i) dp[i][i]=a[i].val;
for(int i=1;i<=n;++i){
int k=i,mx=0;
for(int j=i+1;j<=n;++j){
while(k>=1&&a[j].x-a[i].x>=a[i].x-a[k].x){
mx=max(mx,dp[i][k]); --k;
}
dp[j][i]=max(dp[j][i],mx+a[j].val);
ans=max(ans,dp[j][i]);
}
}
其实第三个和第二个没什么差别,或者和第三个比起来,第二个中的优先队列纯属多余。
其实如果一开始就刷表的话,还是很容易想到如何优化的。可是这个绯雾写填表,试图优化结果自己写挂了……
T6
考虑暴力,找出每个位置能到达最远的点,然后计算,这样是 \(O(n^2)\) 的。
听说有些人用玄学的方法过掉了,比如假设能用上的区间都在前 1000 个,然后 \(O(n^2)\)。
像这种每次向前扩展的,想到倍增,设 \(f_{i,j}\) 表示从位置 \(i\) 经过 \(2^j\) 个区间能到达的最远的点,显然可以预处理 \(f_{i,0}\) 然后 \(O(n\log n)\) 求。
计算答案的时候枚举起点 \(i\),看其是否能到达终点 \(i+n\) (用类似倍增 LCA 后半段的方法),然后若能到达取区间数最小值。
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define maxn 2000010
using namespace std;
int n,m,f[maxn][25],l,r,mid,ans,mx[maxn];
int main(){
scanf("%d%d",&m,&n); ans=inf;
for(int i=1;i<=n;++i){
scanf("%d%d",&l,&r);
if(r<l) r+=m; mx[l]=max(mx[l],r);
}
r=0;
for(int i=1;i<=2*m;++i){
if(mx[i]){
f[i][0]=max(f[i-1][0],mx[i]+1);
r=max(r,mx[i]);
}
else if(r>=i) f[i][0]=f[i-1][0];
else f[i][0]=0;
}
for(int i=1;i<=20;++i)
for(int j=1;j<=2*m;++j)
if(f[j][i-1]) f[j][i]=f[f[j][i-1]][i-1];
for(int i=1;i<=m;++i){
int x=i,s=0;
for(int j=20;j>=0;--j){
if(f[x][j]&&f[x][j]<m+i)
x=f[x][j],s+=(1<<j);
}
if(f[x][0]) ans=min(ans,s+1);
}
if(ans>n) puts("impossible");
else printf("%d\n",ans);
return 0;
}
T7
正在学最小费用最大流。(补上周的坑)
就是线段树优化建图,然后跑费用流,还是比较显然的。
Day 8
今天由于一开始就在 402,所以只听了 S2 没去听 S1。
S2 讲的高斯消元和线性基,最后我还是只会写最基础的……但至少把两年前写的东西复习了一遍。
T1
把我以及不少人搞崩溃的板子题。首先用一种错误的方式(精度不对,加上代码一个小错误)负负得正过去了,然后发现了代码中的小错误,交了一堆五颜六色的 WA 之后,发现是精度问题……
警告:首先这题需要 long double
保证精度,其次 labs
是 long long
的绝对值,fabs
是 float
的绝对值,long double
的绝对值要用 fabsl
,然后 long double
的读入要用 %Lf
。
高斯-约旦消元法的大概过程如下(摘抄自题解):
-
选择一个尚未被选过的未知数作为主元,选择一个包含这个主元的方程。
-
将这个方程主元的系数化为1。
-
通过加减消元,消掉其它方程中的这个未知数。
-
重复以上步骤,直到把每一行都变成只有一项有系数。
然后判断无解或多解有一堆细节。首先应该先判无解。
无解的情况:左边所有项的系数都是 \(1\),且右边的常数项不为 \(0\)。
细节:需要判断当前当前消元消去了几个主元,而非第几个元,若只有个 \(x\) 主元,则只固定前 \(x\) 个方程。
多解的情况:前面有系数为 \(0\),然后常数项不为 \(0\)。
其实真正的高斯消元题也没这么多细节就是了。
NFLSOJ (细节巨多,hack 两遍的)模板题
#include<bits/stdc++.h>
#define maxn 1010
#define eps (1e-10)
using namespace std;
int n,m,nw; long double a[maxn][maxn];
int main(){
scanf("%d%d",&n,&m); nw=1;
for(int i=1;i<=m;++i)
for(int j=1;j<=n+1;++j) scanf("%Lf",&a[i][j]);
for(int i=1;i<=n;++i){
int t=nw;
for(int j=nw;j<=m;++j)
if(fabsl(a[j][i])>fabsl(a[t][i])) t=j;
if(fabsl(a[t][i])>eps)
for(int k=i;k<=n+1;++k)
swap(a[nw][k],a[t][k]);
else continue;
for(int j=n+1;j>=i;--j) a[nw][j]/=a[nw][i];
for(int j=1;j<=m;++j){
if(j==nw) continue;
for(int k=n+1;k>=i;--k)
a[j][k]-=a[j][i]*a[nw][k];
}
++nw;
}
for(int i=nw;i<=m;++i)
if(fabsl(a[i][n+1])>eps){
puts("No solutions"); return 0;
}
for(int i=1;i<=n;++i)
if(fabsl(a[i][i])<eps){
puts("Many solutions"); return 0;
}
for(int i=1;i<=n;++i)
printf("%d\n",int(a[i][n+1]+0.5));
return 0;
}
T2
来源:P4035
高斯消元最简单的应用题(大概)。
直接根据题意列出方程,第 \(i\) 个点和第 \(i+1\) 个点到某点距离相等,共 \(n\) 个方程,稍微化简一下,发现二次项都没了。然后就高斯消元解就行了。
而且没有判无解的麻烦。
其实两年前写过,当时还觉得挺难的。
T6
来源:P3812
线性基最简单的模板题。然而想了好久才理解
关于线性基:一个数组的线性基,满足数组中任意数的异或值,都能表示为线性基中若干个数的异或值。
插入方法:
我们考虑插入的操作,令插入的数为 \(x\),考虑 \(x\) 的二进制最高位 \(i\),
- 若线性基的第 \(i\) 位为 \(0\),则直接在该位插入 \(x\),退出;
- 若线性基的第 \(i\) 位已经有值 \(a_i\),则 \(x=x \oplus ai\),重复以上操作直到 \(x=0\)。
void insert(int x){
for(int i=51;~i;--i){
if(x&(1ll<<i)){
if(!p[i]){p[i]=x; return ;}
else x^=p[i];
}
}
flg=1; //判断异或值是否可能为 0,在后续查询最小值和 k 小值的时候会用到
}
补充:可用类似方法判断数组中是否存在一些数使它们的异或和为 \(x\)。
bool check(int x){
for(int i=51;~i;i--)
if(x&(1ll<<i)){
if(!p[i]) return false;
else x^=p[i];
}
return true;
}
查询最大值:考虑贪心,如果当前位为 \(0\),就异或上线性基当前位的值,这样答案不会变劣。
int qmax(){
int res=0;
for(int i=51;i>=0;--i)
if(!(res&(1ll<<i))) res^=p[i];
return res;
}
查询最小值:若可能为 \(0\) 就返回 \(0\),否则返回线性基中最小的值。
int qmin(){
if(flg) return 0;
for(int i=0;i<=51;++i)
if(p[i]) return p[i];
}
T7
线性基模板 2,题意就是查询某一个数组中选出一些数异或,得到的第 \(k\) 小异或值。
洛谷上找不到原题,但洛谷上模板的第一篇题解顺便讲了这个。
感觉自己不是很理解,只能对照题解把代码写下来。
int query(int k){
int res=0,cnt=0;
k-=flg; if(!k) return 0;
for(int i=0;i<=51;++i){
for(int j=i-1;~j;--j)
if(p[i]&(1ll<<j)) p[i]^=p[j];
if(p[i]) tmp[cnt++]=p[i];
}
if(k>=(1ll<<cnt)) return -1;
for(int i=0;i<cnt;++i)
if(k&(1ll<<i)) res^=tmp[i];
return res;
}
T8
来源:P4869
可以说是线性基模板 3,题意是查询某个值在这个子集所有的异或和中的排名(包括重复的)。
难在发现并证明出现次数是 \(2^{n-k}\)。
这篇题解给出了人话证明。
查询排名的代码如下:
int qrank(int q){
int rk=0,tmp=1;
for(int i=0;i<=51;++i){
if(!p[i]) continue;
if(q&(1ll<<i)) rk=(rk+tmp)%mod;
tmp<<=1;
}
return (rk*cnt+1)%mod; //cnt 为 2^(集合元素个数-线性基元素个数)
}
Day 9
总体情况
1000+1200+1400+1700+1800+840=7940,rk81
由于很多人做过 G,过 G 的比过 F 的人还多,于是又一次被吊打。
T1
很简单,直接计算就行了。(印象中第一天学编程的时候做过这题的弱化版
T2
前缀和。
需要特判 \(s_l\) 为 \(C\),\(s_{l-1}\) 为 \(A\) 的情况。
出题人卡常,用 cin
被卡 T 飞,警钟敲烂。
T3
记录一个前缀最大公约数和后缀最大公约数,枚举。
其实 \(n \geq 2\),并不需要特判 \(n=1\)。
T4
贪心,按照价值排序,判断能不能选。如果能选,尽量向后放。
我考场上用了树状数组 + 二分的双 \(\log\) 做法。就是维护一个长度为 \(m\) 的数组,每个值为 0 或 1(0 表示未选,1 表示已选),然后每次取剩余的价值最大的任务,在能放的区间内,找到最靠后的未被选的一个位置放下。
用树状数组维护这个数组的后缀和(可以把下标倒过来这样就是前缀和了),然后每次查就行了。目前考虑第 \(i\) 个物品,能放的区间是 \(1\) 到 \(m-a_i+1\),二分查找区间内最后一个 \(0\)。但是调了好久。
实际上可以简单的用并查集维护,码量和时间复杂度都吊打我。我是废物。
但好歹在考场上想出来了
#include<bits/stdc++.h>
#define maxn 100010
using namespace std;
struct node{int a,b;}t[maxn];
int n,m,ans=0,s[maxn];
bool cmp(node x,node y){
if(x.b!=y.b) return x.b>y.b;
else return x.a>y.a;
}
int lowbit(int x){return x&(-x);}
void add(int x,int y){
while(x<=m){
s[x]+=y; x+=lowbit(x);
}
}
int ask(int x){
int res=0;
while(x){
res+=s[x]; x-=lowbit(x);
}
return res;
}
bool check(int x,int y){
return ask(x)-ask(y)<x-y;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>t[i].a>>t[i].b;
}
sort(t+1,t+n+1,cmp);
for(int i=1;i<=n;++i){
int l=1,r=m-t[i].a+1,mid;
if(r<1) continue;
while(l<=r){
mid=(l+r)>>1;
if(check(m-mid+1,t[i].a-1)) l=mid+1;
else r=mid-1;
}
if(check(m-r+1,t[i].a-1)&&r)
add(m-r+1,1),ans+=t[i].b;
}
cout<<ans<<endl;
return 0;
}
T5
发现数据很大,考虑数位 dp,分类每一位是否受限制。用传统的数位 dp 板子可以写。
当然也可以简化一点,毕竟只有 \(0\) 和 \(1\)。
对于一位原本是 \(1\),如果此时变为 \(0\),那么后面完全不受限制,有三种情况,\(00\),\(10\),\(11\),而前面受限制,必须与原数完全一样,对于原数为 0 的地方,只有 \(00\),对于原数为 \(1\),可以是 \(01\) 和 \(10\),所以是 \(2\) 的 原数中这一位前面 1 的个数 次方。
如果这一位受限制,就继续考虑下面。
从高位向地位枚举计算答案。最后要加上每一位都受限的答案。
#include<bits/stdc++.h>
#define maxn 100010
#define int long long
#define mod 1000000007
using namespace std;
int n,p=1,ans=0; char s[maxn];
int qpow(int a,int p){
int res=1;
while(p){
if(p&1) res=res*a%mod;
a=a*a%mod; p>>=1;
}
return res%mod;
}
signed main(){
cin>>s; n=strlen(s);
for(int i=0;i<n;++i){
if(s[i]=='0') continue;
ans=(ans+p*qpow(3,n-i-1)%mod);
p=(p<<1)%mod;
}
ans=(ans+p)%mod;
cout<<ans<<endl;
return 0;
}
T6
思路对了,只差一点点就是正解了。考场上不知道哪里写挂掉了。
首先有 \(O(k\cdot n^2)\) 的 dp。设 \(dp_{i,j}\) 表示考虑到第 \(i\) 个数,它为 \(j\) 的方案数,暴力转移。
发现这个 \(dp\) 可以优化成 \(O(n\cdot k)\) 的,对每个 \(i\) 的 dp 数组求一个前缀和,下一次转移的时候直接求 \(1\) 到 \(\lfloor \frac{n}{j} \rfloor\) 的和。
然而这样还是拿不到第二个子任务的分,看到 \(n\leq 10^9\) 说明时间复杂度要么与 \(n\) 无关,要么 \(\sqrt n\) 或者 \(\log_2 n\)。
继续观察 dp 柿子,发现有一个 \(\lfloor \frac{n}{i} \rfloor\),这个东西最多有 \(2\cdot \sqrt n\) 种取值,这不是经典的整除分块吗!于是正解差不多出来了。
然而有一些细节问题导致考场上没调出来,好像逝初始值的问题。
#include<bits/stdc++.h>
#define maxn 100010
#define int long long
#define mod 1000000007
using namespace std;
int n,k,dp[maxn],sum[maxn],sz[maxn],val[maxn],tot=0;
unordered_map<int,int> mp;
signed main(){
cin>>n>>k;
for(int l=1,r;l<=n;l=r+1){
r=n/(n/l); mp[r]=++tot; sum[tot]=r;
val[tot]=r; sz[tot]=r-l+1;
}
for(int i=2;i<=k;++i){
for(int j=1;j<=tot;++j)
dp[j]=sz[j]*sum[mp[n/val[j]]]%mod;
for(int j=1;j<=tot;++j)
sum[j]=(sum[j-1]+dp[j])%mod;
}
cout<<sum[tot]%mod<<endl;
return 0;
}
T7
来源:P4151
有很多人做过这题,导致 G 题通过人数 \(>\) F 题的世界奇观出现,也导致我掉到八十多名。
考虑把这条路径分成一个链和若干个环。(不会出现两个链不然这两个链也会构成环)
然后就是要选出若干个环去走,使链的权值异或上环的权值最大。
对于一个环的权值,定义为除了在链上的部分之外的边的异或和(即起点到整个环第一个在链上的点的异或和,异或上起点到整个环第二个在链上的点的异或和,有点绕,画图看看)。用到异或的性质,一个数异或自己,等于 \(0\)。
然后用线性基维护链的权值(即 \(1\) 到 \(n\) 某条路径的异或和)异或上一些环的权值的最大值。
话说好像找到了南夫拉斯膜你赛的一些规律,前一天讲座讲的算法,第二天膜你赛会考。
(只有我这种废物会错的)注意点:要开 long long
,并且左移的时候要用 1ll<<i
。
Day 10
总体情况
1000+1300+1500+1700+1800+1260=8560,rk36
感觉今天好多数据结构题,然而我只会用树状数组。
T1
数据范围小,直接一个从前往后枚举,一个从后往前枚举。
T2
算是这几天比较难的 T2(?)
规律题,发现如果有偶数个负数,总能通过一些操作把它们都变成正数。
如果有奇数个,最终数列至少有一个负数,从操作次数上也可以看出来。
所以如果偶数个负数,答案为所有数的绝对值之和;如果奇数个,答案为所有数绝对值之和减去绝对值最小的数的绝对值的两倍。
T3
又是结论题,一个棋子若要覆盖一段区间,肯定是从最左边走到最右边,代价为这段区间最右边的点坐标减去最左边的点坐标,并且一个棋子可定是覆盖一段连续的区间,不然一定不优。
然后这题有多个棋子,每个棋子覆盖一段区间,上一个棋子的右端点到下一个棋子的左端点之间可以空出来,要代价最小就要空出来的最大。
所以贪心地选最大的 \(n-1\) 个相邻坐标的差最大的空出来,计算未空出来的长度。
T4
题目就是找一个数前面的第二个比它大的数。
由于本人数据结构学傻了,只会树状数组,于是用树状数组+二分的双 \(\log\) 做法卡过去了,本来以为只能拿 90 分的。(还好常数小,应该是为数不多的只会用双 \(\log\) 硬卡过去的傻子
还好会树状数组,不然只能写常数大代码长的线段树了,还不知道能不能过。
但是我怎么只会树状数组+二分的暴力啊。昨天也写这倒霉东西,看起来像是会数据结构一样其实不过是暴力,被正解吊打
就是先离散化,会离散化出一个排列。(考场上降智,还去重,实际上没有重复的,unique
,lower_bound
纯属多余。
然后记录每个数出现的位置,按从大到小加入,每次查到比它大的前两个的数的位置,记录答案。
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f
#define mod 998244353
#define maxn 1000010
#define int long long
int n,a[maxn],p[maxn],tmp[maxn],tot=0,s[maxn];
int lowbit(int x){return x&(-x);}
void add(int x,int y){
while(x<=n){
s[x]+=y; x+=lowbit(x);
}
}
int ask(int x){
int res=0;
while(x){
res+=s[x]; x-=lowbit(x);
}
return res;
}
bool check(int mid,int pos){
return ask(pos)-ask(mid)<=1;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
read(n);
for(int i=1;i<=n;++i){
read(a[i]); tmp[++tot]=a[i];
}
sort(tmp+1,tmp+tot+1);
tot=unique(tmp+1,tmp+tot+1)-tmp-1;
for(int i=1;i<=n;++i){
a[i]=lower_bound(tmp+1,tmp+tot+1,a[i])-tmp;
p[a[i]]=i;
}
for(int i=n;i>=1;--i){
int l=0,r=p[i]-1,mid;
while(l<=r){
mid=(l+r)>>1;
if(check(mid,p[i])) r=mid-1;
else l=mid+1;
}
a[p[i]]=l+1; add(p[i],1);
}
for(int i=1;i<=n;++i) writeln(a[i]);
return 0;
}
正解可以用单调栈。
T5
发现排名最高时,假设这个人得分是 650,严格强于他的人也是 650,其他人都是 0。除了严格强于他的人,一定排在他前面,手推一下发现其他人一定在他后面,所以最高排名就是严格强于他的人数 \(+1\)。
排名最低的稍微麻烦一些。然后要这个人排名最低,就是严格弱于他的人和他都是 0 分,其他人 650。首先严格弱于他的人一定是在他后面的,然后还有一种特殊情况,就是对于选手 \(i\),选手 \(j\) 有一场比赛和 \(i\) 同分,另一场 \(j\) 是 0 分,\(i\) 是 650 分,这样他们可能同分,\(j\) 可以不排到 \(i\) 前面(手推就可以发现有且只有这一种情况)。
然后考场上我用树状数组维护这个东西,跑得非常慢。
讲课的时候才发现这种数据不大的静态的,直接用二维前缀和就行了。果然我树状数组学傻了,看什么都是树状数组。
二维前缀和就先不写了,其实也不难写(可能比树状数组还好写一点
T6
暴力算法比较好想,就是从叶子结点向上考虑,对于每个节点未被选的维护一个堆,每次回溯时,父节点与所有子节点合并,然后暴力选,复杂度 \(O(n\log n)\) ,能拿 \(60pts\)。
然后需要堆的合并,不会写 fhq-treap,搬出平板电视pbds库。
正解比较妙,是并查集,\(O(n)\) 带一些并查集常数。
对于处理完需求的结点,剩余的直接用并查集并到父节点上满足父节点的需求。
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f
#define mod 998244353
#define maxn 3000010
#define pb emplace_back
#define int long long
int n,m,x,f[maxn],fa[maxn],ans=0,s[maxn];
int find(int x){
if(x!=f[x]) return f[x]=find(f[x]);
return x;
}
void merge(int x,int y){
int u=find(x),v=find(y);
f[u]=v;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
read(n); read(m); f[1]=1;
for(int i=2;i<=n;++i){
read(x); ++x; fa[i]=x; f[i]=i;
}
for(int i=1;i<=m;++i){
read(x); ++x; ++s[x];
}
for(int i=1;i<=n;++i)
if(!s[i]) merge(i,fa[i]);
for(int i=n;i>=1;--i){
x=find(i);
if(!x) continue;
--s[x]; ans+=(i-1);
if(!s[x]) merge(x,fa[x]);
}
writeln(ans);
return 0;
}
T7
来源:P2541
直接枚举,时间复杂度太大,而且会枚举很多重复状态。
因此优化的核心是减少重复状态的枚举,发现对于每个点枚举所有后继是非常浪费时间的,会有很多重复状态。
所以减少后继,考虑以下三种情况:
-
第 \(x\) 个位置变成选第 \(y+1\) 个值。
-
第 \(x\) 个位置选第 \(y\) 个值不再变化,此时开始调整第 \(x+1\) 个位置的值(本来第 \(x+1\) 个位置应该选第 \(1\) 个值,我们此时让它去选第 \(2\) 个值)。
-
第 \(x\) 个位置从选第 \(y\) 个值变为选第 \(1\) 个值,然后第 \(x+1\) 个位置选择第 \(2\) 个值。
对于第三种,需要保证增加的代价非负。所以需要按照 \(p_{x,2}-p_{x,1}\) 升序排序就行了。
第三篇题解还是讲的比较清楚的。
Day 11
倒数第二天了。
在 402 听了 S2 的课,讲的容斥原理和莫反。
T1
容斥原理。如果没有 \(x_i < n\) 的限制,那么这是一个经典的插板法问题。
考虑把不符合限制的减去,钦定一段不合法,计算方案数,减去,发现会多减去一些,所以把两段不合法的加回来,再减去三段的……于是就是容斥了。
钦定 \(i\) 段不合法的答案,可以先取出 \(i\cdot n\) 个球,然后把剩下的划分为 \(m\) 组(可以为 0),然后再从中选出 \(i\) 组把这些球 \(n\) 个打包放回去。所以答案为 \(\binom{k+m-1-n\cdot i}{m-1}\times \binom{m}{i}\)。
不合法段数为奇数就减去,偶数就加上。
注意数组和预处理要到 \(2e5\)。
for(int i=0;i<=m;++i){
if(i&1) ans=(ans-C(k+m-1-i*n,m-1)*C(m,i)%mod+mod)%mod;
else ans=(ans+C(k+m-1-i*n,m-1)*C(m,i)%mod)%mod;
}
T2
同时考虑两维太麻烦了,所以考虑先一行一行地放,这样保证了每行都有,只需要考虑列。
用总的方案数,减去钦定没有某一列的方案数,加上钦定某两列没有的方案数……
答案为
代码就不放了反正式子都放上面了。
T3
来源:P1450
之前写过的容斥题。看起来像是完全背包,但是会超时。
用完全背包可以求出购买价值为 \(i\) ,硬币个数无限的方案数。考虑限制,可以减去不合法的。
所以用总方案数,减去强制一种超限的方案数,加上两种的,减去三种的,再加上四种的。
强制超限可以先强制使用 \(d_i+1\) 个某种硬币,然后剩余随便。一开始的 \(dp\) 已经求出了任意价值随便选的方案数,容斥计算即可。
T4
首先有一个很显然的 \(2^n\) 的容斥方法,然后它很显然地超时了。
发现青蛙 \(i\) 经过的点为 \(\gcd(a_i,m)\) 的倍数,问题转化为求 \([0,m-1]\) 中是 \(\gcd(a_i,m)\) 的倍数的数的个数,而 \(\gcd(a_i,m)\) 一定为 \(m\) 的约数。
考虑容斥,先预处理出 \(m\) 的所有约数,初始时所有 \(\gcd(a_i,m)\) 的贡献为 1,其他为 0,从小到大枚举约数,然后加上一个约数的贡献,然后再从后面所有是这个约数的倍数的数的计算次数中减去这个数的计算次数。
记第 \(i\) 个约数 \(f_i\) 目前被计算的次数为 \(cnt_i\),其贡献为 \(cnt_i\cdot f_i \cdot \frac{(0+\frac{m}{f_i}-1)\cdot \frac{m}{f_i}}{2}\)。
void init(int m){
tot=0;
for(int i=1;i<=sqrt(m);++i){
if(m%i) continue;
f[++tot]=i;
if(i!=m/i) f[++tot]=m/i;
}
sort(f+1,f+tot+1);
}
void solve(int T){
read(n); read(m); ans=0;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;++i){
read(a[i]); a[i]=__gcd(a[i],m);
}
init(m); sort(a+1,a+n+1);
n=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=n;++i)
for(int j=1;j<=tot;++j)
if(f[j]%a[i]==0) cnt[j]=1;
for(int i=1;i<=tot;++i){
ans=(ans+cnt[i]*(m/f[i]-1)*f[i]*(m/f[i])/2);
for(int j=i+1;j<=tot;++j)
if(f[j]%f[i]==0) cnt[j]-=cnt[i];
}
printf("Case #%lld: %lld\n",T,ans);
}
T5
设 \(f_S\) 为集合 \(S\) 所有的连边方案数,\(t_S\) 为使得 \(S\) 不连通的方案数。集合 \(S\) 的答案即为 \(f_S-t_S\).
\(f_S\) 很好求,需要解决的是 \(t_S\) 的计算。考虑固定 \(S\) 中的一个点 \(i\),然后其他点要么与 \(i\) 连通,要么不连通,并且至少有一个点不连通。
枚举与 \(i\) 不连通的子集 \(T\),此时不连通的点之间可以随便连,方案为 \(f_T\),剩余的点之间必须联通,方案数为 \(dp_{\complement_{S}T}\)(全集为 \(S\) 时,集合\(T\) 的补集,不知道这么打对不对)。
所以 \(dp_S= f_S - \sum_{T \subseteq S} f_T \cdot dp_{\complement_{S}T}\)。
从小往大推 \(dp\),可以保证一个集合的子集都已经被计算。最终答案为 \(dp_{2^n-1}\)。
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j){
read(c[i][j]); ++c[i][j];
}
for(int S=0;S<(1ll<<n);++S){
f[S]=1;
for(int i=0;i<n;++i){
if(!(S&(1ll<<i))) continue;
for(int j=i+1;j<n;++j)
if(S&(1ll<<j)) f[S]=f[S]*c[i+1][j+1]%mod;
}
}
for(int S=1;S<(1ll<<n);++S){
int tmp=S^lowbit(S); dp[S]=f[S];
for(int T=tmp;T;T=(T-1)&tmp)
dp[S]=(dp[S]-f[T]*dp[S^T]%mod+mod)%mod;
}
T7
来源:P4318
其实这题似乎没用莫反?就是用了一下莫比乌斯函数。
首先要求第 \(k\) 个,考虑二分。通过一些方法可以发现,答案最大为 \(1644934081\) ,将二分上界设为这个值就可以了。
现在需要解决的问题是 \([1,mid]\) 内有多少数不是完全平方数的倍数。其实只要去掉所有质数的平方的倍数就可以了。
考虑容斥,先去掉所有质数的平方的倍数,然后加上这些质数两两的最小公倍数的平方的倍数……
发现这些数都是若干互异的质数的乘积,容斥的时候是加还是减取决于质数的个数。莫比乌斯函数能很好地解决这一个问题(根据定义)。
所以先用线性筛求一下莫比乌斯函数,然后二分答案。注意预处理的时候要处理到 \(40559\), \(\sqrt{1644934081}\approx 40558\).
T8
来源:P1447
这题其实并不需要莫反。虽然莫反也能做但是觉得太复杂了
这题要求的柿子:
化为
所以就是要求
有一种做法是:
然后后面的东西显然可以用莫比乌斯反演+数论分块 \(O(\sqrt n)\) 求,但是太麻烦了。
这题比较简单的做法是容斥。令 \(g_i\) 表示公因数为 \(i\) 的数对个数(是公因数而不是最大公约数),然后这其中有一些最大公约数是 \(i\) 的 \(2\) 倍及以上的,需要在容斥的时候减掉。
设 \(f_i\) 表示最大公约数为 \(i\) 的数对个数,则
计算 \(f_i\) 然后累计答案。
T9
来源:P2522
这题是经典的莫反。
记 \(solve(n,m) =\sum_{i=1}^n \sum_{i=1}^m [\gcd(i,j)=k]\)
原式等于 \(solve(b,d)-solve(b,c-1)-solve(a-1,d)+solve(a-1,c-1)\)
所以其实就是要求 \(solve(n,m)\),如下:
用莫比乌斯反演,得
变换求和顺序,得
用数论分块求解。
T10
来源:P2257
Day 12
总体情况
1200+1200+1400+1500+1800+630+280=8010,rk38
今天的比赛可能比较难,T2 T3 T4 都调了好久,尤其 T2。
T1
是 \(30\) 的倍数就是要同时是 \(10\) 的倍数和 \(3\) 的倍数,\(10\) 的倍数的特征是最后一位必须是 \(0\),\(3\) 的倍数的特征是所有位之和是 \(3\) 的倍数,又要求最大,所以先把所有数降序排序,再判断是否是 \(30\) 的倍数。
T2
感觉这个 T2 还是又一点难的,至少比之前所有的 T2 都难。
首先一开始读错题了,没看到在连续的地方,自己想了一个 \(O(n^3)\) 的 dp,然后不会写也写不对。
大概过了 10min 才发现题目读错了……重新读题后,发现就是在第一个字符串里面找一段子串,然后插入若干个字符使它等于第二个字符串,对于第一个字符串的每个子串,在第二个字符串上匹配,代价为第二个字符串上没被匹配的字符的代价和。
发现第一个字符串的子串开头确定之后,代价也就确定了。所以枚举第一个字符串子串开头,然后在第二个字符串上匹配。时间复杂度 \(O(nm)\)
具体做法是记录第二个字符串上每一位的下一个每种字符的位置。比较经典。
获得成就:苹果大师
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f3f3f
#define mod 998244353
#define maxn 10010
#define int long long
int n,m,s[maxn],c[4],ans,cnt,nxt[maxn][4],tmp[4],p; char a[maxn],b[maxn];
int idx(char ch){
if(ch=='A') return 0;
if(ch=='C') return 1;
if(ch=='G') return 2;
if(ch=='T') return 3;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
cin>>a>>b; ans=inf;
n=strlen(a); m=strlen(b);
for(int i=n;i>=1;--i) a[i]=a[i-1];
for(int i=m;i>=1;--i) b[i]=b[i-1];
for(int i=0;i<4;++i) read(c[i]);
for(int i=1;i<=m;++i){
s[i]=c[idx(b[i])]; s[i]+=s[i-1];
}
memset(tmp,0x3f,sizeof(tmp));
for(int i=m;i>=0;--i){
for(int j=0;j<4;++j) nxt[i][j]=tmp[j];
tmp[idx(b[i])]=i;
}
for(int i=1;i<=n;++i){
p=0; cnt=0;
for(int j=i+1;j<=n;++j){
if(nxt[p][idx(a[j])]<=m){
cnt+=s[nxt[p][idx(a[j])]-1]-s[p];
p=nxt[p][idx(a[j])];
}
else break;
}
cnt+=(s[m]-s[p]); ans=min(ans,cnt);
}
writeln(ans);
return 0;
}
T3
显然可以算出每个人每天看电视的时间,是中间一段或头尾两段区间。每个电视节目同理。
于是需要维护区间加,查询区间和,当然可以写线段树,但是太麻烦了。
我虽然没有线段树那么麻烦但我还是写了树状数组。
发现这题的修改和查询是分开的,可以差分,每次左端点 \(+1\),右端点的后面 \(-1\),然后每个点的值就是开头到这个点的前缀和。(我就是这里写烦了,我用了树状数组维护前缀和,反正过了)
然后区间查询,就是再做一遍前缀和,然后查就行了。
虽然算法很基础,但是细节多,调了好久。
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f
#define mod 998244353
#define maxn 200010
#define mx 86400
#define int long long
int n,q,l,r,s[maxn],sum[maxn]; string s1,s2; char ch;
int calc(string s){
int res=1;
res+=60*60*((s[0]-'0')*10+(s[1]-'0'));
res+=60*((s[3]-'0')*10+(s[4]-'0'));
res+=((s[6]-'0')*10+(s[7]-'0'));
return res;
}
int lowbit(int x){return x&(-x);}
void add(int x,int y){
while(x<=mx+1){
s[x]+=y; x+=lowbit(x);
}
}
int ask(int x){
int res=0;
while(x){
res+=s[x]; x-=lowbit(x);
}
return res;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
read(n);
for(int i=1;i<=n;++i){
cin>>s1>>ch>>s2;
l=calc(s1); r=calc(s2);
add(l,1),add(r+1,-1);
if(r<l) add(1,1),add(mx+1,-1);
}
for(int i=1;i<=mx+2;++i)
sum[i]=ask(i)+sum[i-1];
read(q);
while(q--){
int f1,f2; cin>>s1>>ch>>s2;
l=calc(s1); r=calc(s2);
if(r<l) f1=sum[r]-sum[0]+sum[mx]-sum[l-1],f2=r+(mx-l+1);
else f1=sum[r]-sum[l-1],f2=r-l+1;
double ans=(double)(f1*1.0)/(f2*1.0);
printf("%.9lf\n",ans);
}
return 0;
}
T4
就是那种,一眼看出来该怎么做,然后又写半天的细节题。
先记录每种字符个数在原字符串的前缀和,想办法算出要求的那行的开头和结尾(可以对 \(len\) 取模但是开头和结尾的差不能变),然后分类讨论,用计算出来前缀和算这种字符有多少个。
然而出题人卡空间,前缀和数组只能开成 \(int\)。
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f
#define mod 998244353
#define maxn 1000010
#define LL long long
int n,q,s[maxn][26]; LL k,m,l,r; string st; char ch;
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
read(n); cin>>st; m=st.length();
for(int i=0;i<m;++i) ++s[i][st[i]-'A'];
for(int i=1;i<m;++i)
for(int j=0;j<26;++j) s[i][j]+=s[i-1][j];
read(q);
while(q--){
read(k); cin>>ch; ch-='A';
if(k&1) l=(k%m)*(((k-1)>>1)%m)%m;
else l=((k>>1)%m)*((k-1)%m)%m;
r=l+k-1;
if(r-l<m-1&&l<=(r%m)) writeln(s[r][ch]*1ll-s[l][ch]*1ll+(st[l]==(ch+'A')));
else writeln(s[m-1][ch]*1ll*(r/m-1)+s[r%m][ch]*1ll+(s[m-1][ch]-s[l][ch])*1ll+(st[l]==(ch+'A')));
}
return 0;
}
T5
容斥题。完美证明了我的猜测,nfls 一定会出前一天讲座讲的算法的题。(而且 T8 是莫反)
首先看到这题想到昨天在题解区看到的一句话:求序列第几个数是几的,二分是个不错的选择。
然后就二分,现在需要快速计算 \([1,mid]\) 中有多少数与 \(n,m\) 都互质。
显然与 \(n,m\) 都互质的数就是不包括和 \(n,m\) 相同的质因子的数。需要先预处理出 \(n,m\) 所有的质因子,然后去重,然后就是经典的容斥。
手算一下,发现 \(10^9\) 之内含有质因子最多的数,最多含有 \(10\) 个左右,而 \(n\) 和 \(m\) 的质因数不重复的话,也就 \(15\) 个左右,不会超过 \(20\),所以直接枚举集合,暴力容斥,复杂度 \(2^{ \text{n 和 m 去重后的的质因数个数}}\) 是可以接受的。
考试的时候随便设了一个 \(10^{14}\) 的二分上界,过了,具体上界是多少也没算,但是就算上界是 \(10^{18}\) 也不会超时。
void init(int n,int m){
int tmp=n; tot=0;
for(int i=2;i<=sqrt(n);++i){
if(tmp%i==0) a[++tot]=i;
while(tmp%i==0) tmp=tmp/i;
}
if(tmp!=1) a[++tot]=tmp;
tmp=m;
for(int i=2;i<=sqrt(m);++i){
if(tmp%i==0) a[++tot]=i;
while(tmp%i==0) tmp=tmp/i;
}
if(tmp!=1) a[++tot]=tmp;
sort(a+1,a+tot+1);
tot=unique(a+1,a+tot+1)-a-1;
}
bool check(int mid){
int res=mid;
for(int i=1;i<(1ll<<tot);++i){
int cnt=0,val=1;
for(int j=0;j<tot;++j)
if(i&(1ll<<j)) ++cnt,val=val*a[j+1];
if(cnt&1) res=res-(mid/val);
else res=res+(mid/val);
}
return res>=k;
}
void solve(){
read(n); read(m); read(k);
l=1; r=1e14; init(n,m);
while(l<=r){
mid=(l+r)>>1;
if(check(mid)) r=mid-1;
else l=mid+1;
}
writeln(l);
}
T6
《正难则反》。
删边需要维护集合的分裂,比较难。考虑倒着加边,这时我们需要算出合并两个集合的匹配星增加的对数。
计算异或路径只需要维护每个点到根节点的异或路径长,两个点之间的异或距离就是这两个点到根节点路径的异或和。
维护这个东西显然可以用带权并查集。(然而我考场上没想到
用 map 记录每个集合到根节点异或和为某值的点的个数。每次用类似启发式合并的方式,把值少的向值多的合并。
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T> inline void write(T x){
if(x<0) putchar('-'),x=-x;
if(x==0){
putchar('0'); return ;
}
if(x>9) write(x/10);
putchar(x%10+'0');
return ;
}
template<typename T> inline void read(T &x){
x=0; int w=1; char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') w=-1; ch=getchar();
}
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x*=w; return ;
}
}
using namespace IO;
#define writesp(x) write(x),putchar(' ')
#define writeln(x) write(x),putchar('\n')
#define inf 0x3f3f3f3f
#define mod 998244353
#define maxn 100010
#define int long long
#define pb emplace_back
struct edge{int u,v,w;}eg[maxn];
int n,x,y,z,f[maxn],dis[maxn],c[maxn],ans[maxn];
map<int,int> mp[maxn];
int find(int x){
if(x!=f[x]){
int rt=find(f[x]);
dis[x]^=dis[f[x]]; f[x]=rt;
return rt;
}
else return x;
}
int merge(int u,int v,int w){
int x=find(u),y=find(v),res=0;
if(mp[x].size()>mp[y].size()) swap(x,y);
f[x]=y; dis[x]^=dis[u]^dis[v]^w;
map<int,int>::iterator it;
for(it=mp[x].begin();it!=mp[x].end();++it){
const int &tmp=it->first^dis[x];
res+=it->second*mp[y][tmp];
mp[y][tmp]+=it->second;
}
mp[x].clear();
return res;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
read(n); ans[n]=0;
for(int i=1;i<n;++i){
read(eg[i].u),read(eg[i].v),read(eg[i].w);
}
for(int i=1;i<n;++i) read(c[i]);
for(int i=1;i<=n;++i) f[i]=i,mp[i][0]=1;
for(int i=n-1;i>=1;--i)
ans[i]=ans[i+1]+merge(eg[c[i]].u,eg[c[i]].v,eg[c[i]].w);
for(int i=1;i<=n;++i) writeln(ans[i]);
return 0;
}
后记
后来一回来就去军训了,军训很累,而且热,开始怀念机房的空调。
军训结束后才来得及补后记,好多事情都忘了,写代码也会犯低级错误,好像几天没碰又有点生疏了。
像往常一样,题目补不完,知识点也不可能完全掌握,毕竟时间有限,内容太多了。
但是在膜你赛被吊打过程中或许实战能力有所提升,比如考场上自己写出容斥,猜结论骗分,对于数据结构和一些算法的理解更深,学会应用等等。
还复习了之前学过的但是半懂不懂的算法,但有些还是仅仅停留在会写模板和最简单的应用。
然后就是留下来一堆坑代填。但是肯定又是还没填完又出现很多新的坑。
总的来说,这次外出集训收获不小,而且很开心,算是弥补了一直集训没出去玩的遗憾。
2023-08-31 “用户已过期”(后来又延迟到 9 月 10 日了)
第一时间想起来把所有视频下载到本地。
10 天时间真的不够我填这么多坑。
所以为什么用户还会过期啊!为什么啊!故意不给补题吗!
2023-09-10 最后一天。
可是还有好多题没补。
暂且就这样吧。
To be continued.