Codeforces Round #837 (Div. 2)
Preface
补题ing
上周由于疫情鸽了好多场,趁现在空下来尽量多写点吧
A. Hossam and Combinatorics
SB题,直接统计下最大的数和最小的数的个数即可
注意所有数相同的情况要特判下
#include<cstdio>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,a[N];
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
for (sort(a+1,a+n+1),i=2;i<=n&&a[i]==a[1];++i);
if (i>n) { printf("%lld\n",1LL*n*(n-1)); continue; }
for (j=n-1;j>=1&&a[j]==a[n];--j);
printf("%lld\n",2LL*(i-1)*(n-j));
}
return 0;
}
B. Hossam and Friends
考虑每一个隔断区间\([l,r]\),若右端点\(R\ge r\),则合法的左端点\(L\)不能\(\le l\)
那么我们可以开一个vector
,记录下每个隔断区间的右端点对应的左端点
然后依次枚举右端点的位置,左端点的最小取值就是经过的所有vector
里元素的最大值再加一
总复杂度\(O(n)\)
#include<cstdio>
#include<vector>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,m,l,r; vector <int> pos[N];
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) pos[i].clear();
for (i=1;i<=m;++i) scanf("%d%d",&l,&r),pos[max(l,r)].push_back(min(l,r));
long long ans=0; int lst=0; for (i=1;i<=n;++i)
{
for (int x:pos[i]) lst=max(lst,x); ans+=i-lst;
}
printf("%lld\n",ans);
}
return 0;
}
C. Hossam and Trainees
SB题,直接分解质因数判断即可
但是要注意\(O(n\sqrt {a_i})\)是会TLE的,因此我们可以先预处理筛出\(\sqrt {a_i}\)范围内的质数,这样复杂度降到\(O(n\times \pi(\sqrt{a_i}))\)即可通过
#include<cstdio>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,a[N],prm[N],cnt; bool vis[N]; set <int> s;
inline void init(CI n)
{
for (RI i=2,j;i<=n;++i)
{
if (!vis[i]) prm[++cnt]=i;
for (j=1;j<=cnt&&i*prm[j]<=n;++j)
if (vis[i*prm[j]]=1,i%prm[j]==0) break;
}
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (init(100000),scanf("%d",&t);t;--t)
{
RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
bool flag=0; for (s.clear(),i=1;i<=n&&!flag;++i)
{
for (j=1;j<=cnt&&1LL*prm[j]*prm[j]<=a[i];++j)
if (a[i]%prm[j]==0)
{
if (s.count(prm[j])) flag=1; s.insert(prm[j]);
while (a[i]%prm[j]==0) a[i]/=prm[j];
}
if (a[i]>1)
{
if (s.count(a[i])) flag=1; s.insert(a[i]);
}
}
puts(flag?"YES":"NO");
}
return 0;
}
D. Hossam and (sub-)palindromic tree
之前好像写过类似的思路的题(还是OI时期了),但是时间太长忘记了
思想就是序列上的区间DP上树的常见套路,最长回文子序列的序列版本很好处理,我们设\(f_{l,r}\)表示区间\([l,r]\)的答案,则:
-
先考虑两个端点跳过的情况,\(f_{l,r}=\max(f_{l+1,r},f_{l,r-1})\)
-
若\(s_l=s_r\),有转移\(f_{l,r}=\max(f_{l,r},f_{l+1,r-1}+2)\)
然后我们发现这个东西拿到树上依然是可以转移的,我们设\(f_{x,y}\)表示树上两点\(x,y\)之间的答案
不难发现如果\(x,y\)没有祖先关系,上面的\(l+1,r-1\)对应的就是\(x,y\)的父节点
如果有祖先关系(设\(x\)是\(y\)的祖先),那么区别就是我们此时要找到\(x\)在\(y\)方向的儿子来转移
这部分可以在树上倍增结合LCA实现,由于这种区间DP在序列上的复杂度是\(O(n^2)\)的
我们把它搬到树上也就多了每次转移一个\(\log\)的复杂度,因此总复杂度\(O(n^2\log n )\)
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=2005,P=12;
int t,n,x,y,ans,dep[N],anc[N][P],f[N][N]; char s[N]; vector <int> v[N];
inline void DFS(CI now=1,CI fa=0)
{
dep[now]=dep[fa]+1; anc[now][0]=fa;
for (RI i=0;i<P-1;++i) anc[now][i+1]=anc[anc[now][i]][i];
for (int to:v[now]) if (to!=fa) DFS(to,now);
}
inline int getLCA(int x,int y)
{
if (dep[x]<dep[y]) swap(x,y);
for (RI i=P-1;~i;--i) if (dep[anc[x][i]]>=dep[y]) x=anc[x][i];
if (x==y) return x;
for (RI i=P-1;~i;--i) if (anc[x][i]!=anc[y][i])
x=anc[x][i],y=anc[y][i]; return anc[x][0];
}
inline int jump(int x,CI y)
{
for (RI i=0;i<P;++i) if ((y>>i)&1) x=anc[x][i]; return x;
}
inline int DP(CI x,CI y)
{
if (!x||!y) return 0;
if (~f[x][y]) return f[x][y]; if (x==y) return f[x][y]=1;
int fa=getLCA(x,y),fx=anc[x][0],fy=anc[y][0];
if (fa==x) fx=jump(y,dep[y]-dep[x]-1);
else if (fa==y) fy=jump(x,dep[x]-dep[y]-1);
f[x][y]=max(DP(fx,y),DP(fy,x));
if (s[x]!=s[y]) return f[x][y];
if (fx==y&&fy==x) f[x][y]=max(f[x][y],2);
else f[x][y]=max(f[x][y],DP(fx,fy)+2); return f[x][y];
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i,j; for (scanf("%d%s",&n,s+1),i=1;i<=n;++i)
for (v[i].clear(),j=1;j<=n;++j) f[i][j]=-1;
for (i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for (i=1;i<=n;++i) for (j=0;j<P;++j) anc[i][j]=0;
for (ans=0,DFS(),i=1;i<=n;++i) for (j=1;j<=n;++j)
ans=max(ans,DP(i,j)); printf("%d\n",ans);
}
return 0;
}
E. Hossam and a Letter
纯暴力枚举题,但是有些细节容易写挂(不过写的时候感觉比较好,编译过了就直接过了)
考虑形成一个H的关键,其实就是要确定转折处的那两个位置的坐标
换句话说,我们可以枚举\(x,y,z\),表示H的横行在第\(x\)行,两个纵列分别在\(y,z\)
这样我们只需要知道两个转折点分别最多向上和向下延申多少即可
不难想到预处理一下,\(up_{0/1,i,j}\)表示从\((i,j)\)往上(包括自身),经过\(0/1\)个m最多可以延申几个格子
同理\(down_{0/1,i,j}\)表示从\((i,j)\)往下(不包括自身),经过\(0/1\)个m最多可以延申几个格子
然后根据中间的横行用了多少个m分类讨论下就完了,细节可以看代码
复杂度\(O(n^3)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=405;
int n,m,up[2][N][N],down[2][N][N],ans; char s[N][N];
inline void check(CI A,CI B,CI C,CI D,CI x,CI y,CI z)
{
int Up=min(up[A][x][y],up[B][x][z]),Down=min(down[C][x][y],down[D][x][z]);
if (Up>1&&Down>=1) ans=max(ans,2*(Up+Down)+z-y-1);
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i,j,k; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%s",s[i]+1);
for (i=1;i<=n;++i) for (j=1;j<=m;++j)
{
if (s[i][j]=='#') continue; int ct=0;
for (k=i;k>=0;--k)
{
if (k==0)
{
if (ct==1) up[ct][i][j]=i;
else up[0][i][j]=up[1][i][j]=i; break;
}
if (s[k][j]=='#') { up[ct][i][j]=i-k; break; }
else if (s[k][j]=='m')
{
up[ct][i][j]=i-k; ++ct;
if (ct>1) break;
}
}
if (!ct) up[1][i][j]=up[0][i][j];
for (ct=0,k=i+1;k<=n+1;++k)
{
if (k==n+1)
{
if (ct==1) down[ct][i][j]=n-i;
else down[0][i][j]=down[1][i][j]=n-i; break;
}
if (s[k][j]=='#') { down[ct][i][j]=k-i-1; break; }
else if (s[k][j]=='m')
{
down[ct][i][j]=k-i-1; ++ct;
if (ct>1) break;
}
}
if (!ct) down[1][i][j]=down[0][i][j];
}
for (j=1;j<m-1;++j) for (i=2;i<=n-1;++i)
{
if (s[i][j]=='#') continue;
int ct=0; for (k=j+1;k<m;++k)
{
if (s[i][k]=='#') break;
if (s[i][k]=='m') ++ct; if (ct>1) break;
if (ct==1) check(0,0,0,0,i,j,k+1);
else check(1,0,0,0,i,j,k+1),check(0,1,0,0,i,j,k+1),
check(0,0,1,0,i,j,k+1),check(0,0,0,1,i,j,k+1);
}
}
return printf("%d",ans),0;
}
F. Hossam and Range Minimum Query
没想到可以通过把数随机映射来避免被构造数据卡,学到了学到了
首先如果离线的话就是个莫队裸题,但强制在线的话一般区间询问就要考虑主席树了
但是主席树里应该维护什么信息呢,总不可能要记下每个数出现的个数吧
考虑题目中询问的出现次数是奇数的限制,我们不难把它和异或联系起来
我们考虑维护一颗主席树,节点存储区间的异或和,不难发现由于异或有可减性因此是可行的
那么我们只要判断某个离散化后的值域区间的值是否为\(0\)即可
但想到这里容易发现对于形如1 2 3
这样的数据处理到就GG了,遂去看Tutorial
随即发现由于我们在维护的时候不关心数具体是什么,只关心它是否重复
因此可以给每个数随机赋一个unsigned long long
范围的值,这样撞车的概率就很小了
总复杂度\(O(n\log n)\)
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#define RI register int
#define CI const int&
using namespace std;
typedef unsigned long long u64;
const int N=200005;
int n,q,a[N],rst[N],cnt,l,r,ans,rt[N]; u64 rnd[N];
class Chairman_Tree
{
private:
struct segment
{
u64 val; int ch[2];
}node[N*30]; int tot;
public:
#define V(now) node[now].val
#define ls(now) node[now].ch[0]
#define rs(now) node[now].ch[1]
#define TN CI l=1,CI r=cnt
inline void insert(int& now,CI lst,CI pos,const u64& val,TN)
{
now=++tot; node[now]=node[lst]; V(now)^=val; if (l==r) return;
int mid=l+r>>1; if (pos<=mid) insert(ls(now),ls(lst),pos,val,l,mid);
else insert(rs(now),rs(lst),pos,val,mid+1,r);
}
inline int query(CI x,CI y,TN)
{
if (V(x)==V(y)) return 0; if (l==r) return l; int mid=l+r>>1;
if (V(ls(x))!=V(ls(y))) return query(ls(x),ls(y),l,mid);
else return query(rs(x),rs(y),mid+1,r);
}
#undef V
#undef ls
#undef rs
#undef TN
}T;
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]),rst[i]=a[i];
sort(rst+1,rst+n+1); cnt=unique(rst+1,rst+n+1)-rst-1; srand(time(0));
for (i=1;i<=cnt;++i) rnd[i]=1ull*rand()*rand()*rand()*rand()*rand();
for (i=1;i<=n;++i) a[i]=lower_bound(rst+1,rst+cnt+1,a[i])-rst,T.insert(rt[i],rt[i-1],a[i],rnd[a[i]]);
for (scanf("%d",&q),i=1;i<=q;++i)
{
scanf("%d%d",&l,&r); l^=ans; r^=ans;
printf("%d\n",ans=rst[T.query(rt[l-1],rt[r])]);
}
return 0;
}
Postscript
闪总啊闪总,你不能再沉迷推Gal了!速度写题!!!