恩欧挨批模拟试题-15
水博客太快乐了
考场
先看 \(T1\) ,这数据范围和题目描述,显然是个数学题。
考虑抄题解,但是现在是考试时间,所以考虑放弃。
再看 \(T2\) ,这显然可以用淀粉质点分治来写。
这不是个点分治裸题???
从开考半个小时开始写,大概写了一个半小时,终于水过了大样例,自信交了。
感觉这题貌似会有很多人过。。。。
最后看 \(T3\) ,仔细分析后发现是个线段树,剩下的时间全部用来写线段树,水过了样例,此时距离考试结束还有 \(32s\) ,感觉貌似能过。
分数
预估 : \(t1\) \(0pts\) \(+\) \(t2\) \(100pts\) \(+\) \(t3\) \(100pts\) \(=\) \(200pts\)
实际 : \(t1\) \(0pts\) \(+\) \(t2\) \(100pts\) \(+\) \(t3\) \(0pts\) \(=\) \(100pts\)
发现 \(t2\) 拿了首 \(A\) 。
发现 \(t3\) 挂成了零分???
仔细看代码发现 \(update\) 写错了。。。。
改了就过了。。。。在考试结束后 \(6min\) 水过了这道题。。。。
题解
A. 夜莺与玫瑰
乍一看就不像是在考场上会做的题。。。
考完看题解。。。发现其实没想象中那么难。
平行于坐标轴的直线显然有 \(n+m\) 条,之后枚举斜率,只考虑斜率为正的情况,最后只需将答案乘 \(2\) 即可。
用 \((a, b)\) 表示斜率,显然只需要考虑 \(gcd(a,b)=1\) 的情况( \(gcd(a,b) \ne 1\) 的情况显然已经被讨论过了)。
对于点 \((x, y)\) ,设它的前驱为 \((x-a, y-b)\) , 后继为 \((x+a, y+b)\) ,那么只有当这个点的前驱与这个点均在矩形范围内且这个点的后继不在范围内才将这个点计入贡献。。
貌似不是很好理解,看下图。。。
若点 \((x, y)\) 处于绿色区域,则满足上述条件,将其计入贡献,若点 \((x, y)\) 处于红色区域,则其前驱一定不在矩形内,若点 \((x, y)\) 处于黄色区域,则其后继一定也在矩形内,而这样构成的直线一定已经被绿色区域内的点计算过一次了,再算显然就算重了。
因此要算的显然是当 $gcd(a, b)=1) 时绿色区域的点对个数。。。
得到式子:
\(\sum_{i=1}^{n-1} \sum_{j=1}^{m-1} [gcd(i,j)=1](n-i)(m-j)-max(n-2i,0) \times max(m-2j,0)\)
直接求显然会 \(T\) ,考虑优化,拆式子:
\(\sum_{i=1}^{n-1} \sum_{j=1}^{m-1} [gcd(i,j)=1]m(n-i)-j(n-i)\)
显然可以求一个前缀和,\(tot_{i,j}\) 表示从 \(1\) 到 \(j\) 与 \(i\) 互质的数的个数, \(sum_{i,j}\) 表示这所有与 \(i\) 互质的数的和。
这样,原式可化为:
\(\sum_{i=1}^{n-1} m(n-i) \times tot_{i,m-1}-(n-i) \times sum_{i,m-1}\)
\(hoho\) ,变成了每次询问 \(O(n)\) 处理。(其实还可以优化成每次询问 \(O(1)\) 处理,不过空间卡的很死。。。式子的后半部分同理。。。。
这样 \(O(n^{2})\) 预处理每次询问 \(O(n)\) 处理,总时间复杂度 \(O(n^{2}+Tn)\) 时间开到了 \(2s\) 显然可过。
代码中用到了一个科技,可以在 \(O(nm)\) 时间内求出任意 \((n,m)\) 内任意两个数的 \(gcd\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N=4010, mod=(1<<30);
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int t, n, m;
long long ans;
unsigned int sum[N][N];
unsigned short gcd[N][N], tot[N][N];
inline void pre(){
for(int i=1; i<=4000; ++i) gcd[1][i]=gcd[i][1]=1, gcd[i][0]=i;
for(int i=1; i<=4000; ++i){
for(int j=1; j<=4000; ++j){
gcd[i][j]=gcd[min(i, j)][max(i, j)%min(i, j)];
sum[i][j]=sum[i][j-1]; tot[i][j]=tot[i][j-1];
if(gcd[i][j]==1) tot[i][j]++, sum[i][j]+=j;
}
}
}
int main(void){
pre(); t=read();
while(t--){
n=read(), m=read(); ans=0;
if(n>m) n^=m, m^=n, n^=m;
for(int i=1; i<n; ++i){
ans+=1ll*(n-i)*(1ll*tot[i][m-1]*m-sum[i][m-1])%mod; ans%=mod;
if(n>(i<<1)) ans-=((1ll*(n-(i<<1))*(1ll*tot[i][m>>1]*m-(sum[i][m>>1]<<1)))%mod+mod)%mod;
ans=(ans%mod+mod)%mod;
}
printf("%lld\n", ((ans<<1)+n+m)%mod);
}
return 0;
}
B. 影子
淀粉质点分治裸题,记录一下代码。。。。
貌似没啥可说的。。。
就直接点分治,把每条路径以该路径上最小点权为下标,将其路径长度塞到树状数组中,树状数组维护最大值,对于每条路径在树状数组中找一条最小值大于当前路径最小值且长度尽可能长的路径,更新答案,为防止某些情况没有找到,在每个重心跑完后,要将子树顺序反过来再跑一次。
时间复杂度 \(O(nlog^{2}n)\) ,且常数大的离谱。
code
#include<bits/stdc++.h>
using namespace std;
#define t first
#define w second
#define int long long
const int N=1e5+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
#define lb(x) x&-x
class TRE{
private :
int a[N], n;
public :
inline void add(int p, int x) { for(; p<=n; p+=lb(p)) a[p]=max(a[p], x); }
inline int query(int p) { int ans=0; for(; p; p-=lb(p)) ans=max(a[p], ans); return ans; }
inline void init(int x) { n=x; }
inline void clean(int p) { for(; p<=n; p+=lb(p)) a[p]=0; }
}t1;
#undef lb
int T, n;
int val[N], mx[N], siz[N], top, b[N], rt, ans;
pair<int, int > stk[N];
vector<pair<int, int > > l[N];
bool vis[N];
inline void init(int x){
for(int i=1; i<=x+1; ++i) l[i].clear();
memset(vis, 0, (x+1)*sizeof(bool));
ans=top=rt=0; mx[0]=0x7fffffffff;
}
void Find1(int u, int fa, int s){
siz[u]=1; mx[u]=0;
for(pair<int, int> v : l[u]){
if(v.t==fa||vis[v.t]) continue;
Find1(v.t, u, s); siz[u]+=siz[v.t];
mx[u]=max(siz[v.t], mx[u]);
}
mx[u]=max(mx[u], s-siz[u]+1);
if(mx[u]<mx[rt]) rt=u;
}
void get_dis(int u, int fa, int len, int minn){
siz[u]=1; stk[++top]=make_pair(minn, len);
ans=max(ans, (t1.query(n-minn+1)+len)*b[minn]);
for(pair<int, int > v : l[u]){
if(v.t==fa||vis[v.t]) continue;
get_dis(v.t, u, len+v.w, min(minn, val[v.t]));
siz[u]+=siz[v.t];
}
}
void work(int u){
int now; top=0;
for(int i=0; i<l[u].size(); ++i){
pair<int, int > v=l[u][i];
if(vis[v.t]) continue;
now=top; get_dis(v.t, u, v.w, min(val[v.t], val[u]));
for(int j=now+1; j<=top; ++j) t1.add(n-stk[j].t+1, stk[j].w);
}
while(top) t1.clean(n-stk[top--].t+1);
for(int i=l[u].size()-1; ~i; --i){
pair<int, int > v=l[u][i];
if(vis[v.t]) continue;
now=top; get_dis(v.t, u, v.w, min(val[v.t], val[u]));
for(int j=now+1; j<=top; ++j) t1.add(n-stk[j].t+1, stk[j].w);
}
while(top) t1.clean(n-stk[top--].t+1);
}
void solve(int u){
vis[u]=1; work(u);
for(pair<int, int > v : l[u]){
if(vis[v.t]) continue;
rt=0; mx[0]=0x7fffffffff;
Find1(v.t, u, siz[v.t]);
solve(rt);
}
}
signed main(void){
T=read();
while(T--){
n=read(); int x, y, z;
t1.init(n+1); init(n+1);
for(int i=1; i<=n; ++i) b[i]=val[i]=read();
sort(b+1, b+1+n); int len=unique(b+1, b+1+n)-b-1;
for(int i=1; i<=n; ++i) val[i]=lower_bound(b+1, b+1+len, val[i])-b;
for(int i=1; i<n; ++i){
x=read(), y=read(), z=read();
l[x].push_back(make_pair(y, z));
l[y].push_back(make_pair(x, z));
}
Find1(1, 0, n); solve(rt);
printf("%lld\n", ans);
}
return 0;
}
这个方法被并查集的单 \(log\) 按在地上踩。。。。
C. 玫瑰花精
思考问题的本质是什么。。。
实际上就是要找已经标记的相邻两点之间距离的最大值,显然可以用线段树维护。
每个节点只需维护 \(maxn, l1, r1, p\) 四个数,分别表示当前区间内相邻两点距离的最大值,这两点的中点,该区间最靠左和最靠右的被表示的点分别是哪。每个操作取出一个点,或放如一个点。
当然还有两种情况,就是直接将新点放在 \(1\) 或 \(n\) 的位置上,直接判即可。
时间复杂度 \(O(nlogm)\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m, num, l, r;
int pos[N];
bool vis[N];
bool flag;
struct TRE{
int l, r, l1, r1;
int maxn, p, num;
}t[N<<2];
void built(int l, int r, int p){
t[p].l=l, t[p].r=r; t[p].l1=n, t[p].r1=1;
if(l==r) return; int mid=(l+r)>>1;
built(l, mid, p<<1); built(mid+1, r, p<<1|1);
}
inline void upd(int p){
int ls=p<<1, rs=p<<1|1;
t[p].maxn=t[rs].maxn;
t[p].p=t[rs].p;
if(((t[rs].l1-t[ls].r1)>>1)>=t[p].maxn&&t[ls].num&&t[rs].num){
t[p].maxn=(t[rs].l1-t[ls].r1)>>1;
t[p].p=(t[rs].l1+t[ls].r1)>>1;
}
if(t[ls].maxn>=t[p].maxn) t[p].maxn=t[ls].maxn, t[p].p=t[ls].p;
}
void cut(int x, int p){
t[p].num++;
t[p].l1=min(t[p].l1, x);
t[p].r1=max(t[p].r1, x);
if(t[p].l==t[p].r) { t[p].maxn=0; return; }
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) cut(x, p<<1);
else cut(x, p<<1|1);
upd(p);
}
void merge(int x, int p){
t[p].num--;
if(!t[p].num) t[p].r1=1, t[p].l1=n, t[p].num=0;
if(t[p].l==t[p].r) return;
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) merge(x, p<<1);
else merge(x, p<<1|1);
if(x==t[p].l1&&t[p].num){
if(t[p<<1].num) t[p].l1=t[p<<1].l1;
else t[p].l1=t[p<<1|1].l1;
}
if(x==t[p].r1&&t[p].num){
if(t[p<<1|1].num) t[p].r1=t[p<<1|1].r1;
else t[p].r1=t[p<<1].r1;
}
upd(p);
}
int main(void){
n=read(), m=read();
built(1, n, 1); l=n+1, r=0;
int id, x, maxn;
while(m--){
id=read(), x=read();
maxn=0;
if(id==1){
if(!vis[1]) maxn=l-1; pos[x]=1; ++num;
if((t[1].maxn>maxn&&num>=2)||!pos[x]) maxn=t[1].maxn, pos[x]=t[1].p;
if(n-r>maxn&&!vis[n]) maxn=n-r, pos[x]=n;
cut(pos[x], 1); l=t[1].l1; r=t[1].r1;
vis[pos[x]]=1;
printf("%d\n", pos[x]);
}else merge(pos[x], 1), l=t[1].l1, r=t[1].r1, --num, vis[pos[x]]=0, pos[x]=0;
if(!num) l=n+1, r=0;
}
return 0;
}