D
平方数
求有多少对数乘起来是完全平方数。
典,最简单的做法是,把每个数字的每个因子,都只保留到1个,也就是把每个数里的完全平方数因子都去掉。此时剩余的不妨称为一个数字的核,两个数的核相同,乘起来就是完全平方数。
因为他们的完全平方数部分乘起来还是完全平方数,可以忽略,不是完全平方数的部分,相同,乘起来每个因子都是偶数次,也是完全平方数。
对核计数即可
int n,k;
map<int,int>mp;
int get(int x){
int ans=1;
rep(i,2,sqrt(x)){
if(x%i==0){
int cnt=0;
while(x%i==0)x/=i,cnt++;
if(cnt%2)ans*=i;
}
}
if(x!=1)ans*=x;
return ans;
}
void solve(void){
cin>>n;
int ans=0,cnt0=0;
rep(i,1,n){
int t;cin>>t;
if(t==0)cnt0++;
else{
int x=get(t);
ans+=mp[x];
mp[x]++;
}
}
cout<<ans+cnt0*(n-1)-(cnt0-1)*cnt0/2;
}
E
反图 dij
对每个点求能达到n的最晚出发时间。最晚出发时间不好求,考虑从n出发,时光倒流,求就能转化成从n出发的最短路,这就是单源最短路,可以dij
直接建反图。松弛操作,由于这个车是周期性来的,需要特殊的转移
int cnt,head[N],n,m;
bool vis[N];
ll d[N];
struct edge{
int l,d,k,c,v;
};
priority_queue<pii>q;
vector<edge>g[N];
void dij(int s){
d[s]=2e18;
q.push({2e18,s});
while(!q.empty())
{
int x=q.top().second;//取堆顶编号
q.pop();
if(vis[x])continue;//访问过,直接删掉
vis[x]=1;//标记为访问过中
for(auto i:g[x])
{
if(d[x]<i.d+i.c)continue;
int k=(d[x]-i.l-i.c)/i.d;
k=min(k,i.k-1);
if(d[i.v]<i.l+k*i.d)
{
d[i.v]=i.l+k*i.d;//松弛
q.push({d[i.v],i.v});//相邻点入队
}
}
}
}
void solve(void){
cin>>n>>m;
rep(i,1,m){
int l,d,k,c,a,b;
cin>>l>>d>>k>>c>>a>>b;
g[b].push_back({l,d,k,c,a});
}
dij(n);
rep(i,1,n-1){
if(d[i]==0)cout<<"Unreachable\n";
else cout<<d[i]<<'\n';
}
}
F
概率dp 线段树优化
两个人的胜负和对方点数有关,但是每个人投出点数为iii的概率是和对方无关的,可以分开计算。
设投出点数为iii的概率为gig_igi,gig_igi会给[i+1,i+D][i+1,i+D][i+1,i+D]都有贡献,且贡献都为giD\frac{g_i}{D}Dgi,这就是个区间加,单点查,可以线段树优化dpdpdp。对手还需要i<Li<Li<L时一直投,所以[0,L−1][0,L-1][0,L−1]的部分只在转移时有用,最后要清零。
投出点数的概率算完了,用它来求获胜的概率。当前点数iii,要么停下,要么接着投。设获胜概率为fif_ifi,答案就是停下和接着投取个max\maxmax
停下且获胜,需要对手配合,也就是对手投出的点数比iii小,或者超过nnn,这两种情况的概率都是对gig_igi的区间求和,考虑gig_igi的前缀和sis_isi,答案就是si−1+1−sns_{i-1}+1-s_nsi−1+1−sn
接着投,相当于转移到了子问题,概率为子问题获胜概率的和1D∑j=i+1i+Dfj\frac {1}{D}\sum_{j=i+1}^{i+D}f_jD1∑j=i+1i+Dfj
所以这个dpdpdp需要倒推,并且这个转移也是个区间和,但是只有查询,不用线段树,手动维护后面DDD个位置的和即可。
int n,l,d;
db g[N],dp[N];
struct Fenwick {
double c[N];
int lowbit(int x){
return x&(-x);
}
void upd(int l, int r, double x) {
for (int i = l + 1; i < N; i += lowbit(i))
c[i] += x;
for (int i = r + 2; i < N; i += lowbit(i))
c[i] -= x;
}
double ask(int x) {
double ans = 0;
for (int i = x + 1; i > 0; i -= lowbit(i))
ans += c[i];
return ans;
}
} fenwick;
db cal(int x){
if(x>n)return 0.0;
db ans=1-g[n];
if(x>l)ans+=g[x-1];
return ans;
}
void solve(void){
cin>>n>>l>>d;
fenwick.upd(0,0,1);
rep(i,0,N-5){
g[i]=fenwick.ask(i);
if(i<l){
fenwick.upd(i+1,i+d,g[i]/d);
g[i]=0;
}
}
rep(i,1,N-5)g[i]+=g[i-1];
db sum=0;
rep1(i,N-5,0){
if(i>n)dp[i]=0;
else dp[i]=max(sum/d,cal(i));
sum+=dp[i];
if(i+d<=N-5)sum-=dp[i+d];
}
printf("%.15lf",dp[0]);
}
G
标记永久化线段树
有撤销,所以不能把操作下放。下放的话,再想撤销就要下到所有下方的子区间去撤销,破坏了线段树拆分区间只有O(logn)O(\log n)O(logn)的性质。
那不下放能处理修改和查询吗?能的,使用标记永久化的思想,每次修改,都在经过的所有节点里加上这次操作,查询时累加所有经过的节点的操作即可。
对于这道题,就是可以在每个节点维护一个可动态排序的数据结构,每次修改取maxmaxmax,把maxmaxmax值插入节点的数据结构。查询时,把经过节点都查询最大值,取maxmaxmax,就考虑了所有能影响查询位置的操作的最大值。
标记永久化由于不用下放,上传,常被用于维护主席树的修改操作。
int n;
int x[N],l[N],r[N];
struct Tree{
struct Node{
int l,r;
multiset<pair<ll,ll>>s;
}tr[N<<2];
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;
tr[u].s.clear();
if(l==r){
return;
}
int mid=(l+r)>>1;
build(ls,l,mid); build(rs,mid+1,r);
}
void modify(int u,int l,int r,int val,int id){
if(r<tr[u].l||tr[u].r<l)return;
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].s.insert({val,id});
return ;
}
else{
modify(ls,l,r,val,id);
modify(rs,l,r,val,id);
}
}
void del(int u,int l,int r,int val,int id){
if(r<tr[u].l||l>tr[u].r)return;
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].s.erase({val,id});
return ;
}
else{
del(ls,l,r,val,id);
del(rs,l,r,val,id);
}
}
ll query(int u,int x){
ll mx=0;
if(!tr[u].s.empty()){
mx=max(mx,(*tr[u].s.rbegin()).first);
}
if(tr[u].l==tr[u].r) return mx;
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid)return max(mx,query(ls,x));
else return max(mx,query(rs,x));
}
}t;
void solve(void){
cin>>n;
t.build(1,1,n);
rep(i,1,n){
int a;
cin>>a;
t.modify(1,i,i,a,0);
}
int q;
cin>>q;
rep(i,1,q){
int op;cin>>op;
if(op==1){
cin>>l[i]>>r[i]>>x[i];
t.modify(1,l[i],r[i],x[i],i);
}
else if(op==2){
int p;
cin>>p;
t.del(1,l[p],r[p],x[p],p);
}
else {
int p;
cin>>p;
cout<<t.query(1,p)<<'\n';
}
}
}