====== 2020牛客暑期多校训练营(第二场) ======
===== 比赛情况 =====
^题号 ^A ^B ^C ^D ^E ^F ^G ^H ^I ^J ^K |
^状态 |Ø |Ø |O |O |Ø |O |Ø |Ø |Ø |Ø |Ø |
//O 在比赛中通过 Ø 赛后通过 ! 尝试了但是失败了 - 没有尝试//
**比赛时间**
2020-07-13 12:00-17:00
===== 题解 =====
==== A - All with Pairs ====
给出$n$个字符串,定义$f(s,t)$为$s$的前缀与$t$的后缀相等的最大长度。统计$\sum\limits _{i=1}^n\sum\limits _{j=1}^nf(s_i,s_j)$。
可以先对所有串的所有后缀哈希,则对于每个串,可以得到第$i$位前缀与$\text{cnt}[i]$个后缀相同,但这个前缀未必是长度最大的,kmp一下$\text{cnt}[\text{nxt}[i]]-=\text{cnt}[i]$即可得到每一位的真正贡献。
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include
using namespace std;
#define ll long long
const int N=1e5+5;
const int M=1e6+5;
const ll mod=998244353;
string s[N];
int nxt[M],cnt[N];
char p[M];
mapmp;
ll res,pw[M];
int main()
{
int n;
scanf("%d",&n);
pw[0]=1;for(int i=1;i>s[i];
ll hash=0,m=s[i].length();
for(int j=1;j<=m;j++,hash*=113)
hash+=s[i][m-j]-'a'+1,mp[hash]++;
}
for(int k=1;k<=n;k++)
{
int m=s[k].length();
s[k].copy(p+1,m,0),p[m+1]='\0';
ll hash=p[1]-'a'+1;cnt[1]=mp[hash];
for(int i=2,j=0;i<=m;i++)
{
while(j&&p[i]!=p[j+1])j=nxt[j];
if(p[i]==p[j+1])j++;
hash+=pw[i-1]*(p[i]-'a'+1),cnt[i]=mp[hash];
nxt[i]=j;
if(j)cnt[j]-=cnt[i];
}
for(int i=1;i<=m;i++)res=(res+1ll*cnt[i]*i%mod*i%mod)%mod;
}
printf("%lld\n",res);
return 0;
}
\\
==== B - Boundary ====
问过原点的圆周里过给定点数量的最大值。
$n\le 2000$,想到选各点作为圆上一点$P$则圆心在$OP$的中垂线上,对于中垂线两两求交点看重合次数。map查询$O(n^2\log n)$
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include
#define ll long long
using namespace std;
const int N=2e3+10;
const double eps=1e-5;
const double INF=(1LL<<30);
inline ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
inline ll lcm(ll a,ll b){return a*b/gcd(a,b);}
int dcmp(double x,double y)
{
double t=fabs(x-y);
if(t<-eps)return -1;
return t>eps;
}
struct Point
{
double x,y;
Point(double x=0,double y=0):x(x),y(y){}
bool operator < (const Point &a) const {return dcmp(x,a.x)==0?(ymp;
inline ll read()
{
ll x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int main()
{
int n=read(),res=0;
for(int i=1;i<=n;i++)
{
p[i].x=read(),p[i].y=read();
Point mid=middle(p[i],ori);
l[i].a=p[i].x,l[i].b=p[i].y;
l[i].c=-l[i].a*mid.x-l[i].b*mid.y;
mp.clear();
for(int j=1;j
\\
==== C - Cover the Tree ====
题意:最少链覆盖树,输出方案。
首先毫无疑问的是最少链数就是度数为1的节点数目加一除以二,方案的话,对于x的子树中度数为1的节点,如果目前未匹配的有奇数个,那么他们之间可以两两配对,然后留一个伸出去,从而覆盖x到其父亲的边,偶数个则要伸出去两个,这样贪心的输出方案即可。
#include
#include
#include
#include
#include
using namespace std;
const int N = 2e5+5;
int head[N],tot;
int du[N];
struct E
{int nxt,to;}e[N<<1];
void add(int x,int y) {
e[++tot].nxt = head[x];head[x] = tot;e[tot].to = y;
e[++tot].nxt = head[y];head[y] = tot;e[tot].to = x;
}
queuesons[N];
int anss[N][2],anscnt;
void dfs(int x,int fa) {
bool isson = true;
int tomatch = 0;
for (int i = head[x];i;i=e[i].nxt)
if (e[i].to!=fa) {
dfs(e[i].to,x);
isson = false;
tomatch += sons[e[i].to].size();
}
if (isson) {
sons[x].push(x);
}
queueson2,son1;
for (int i = head[x];i;i=e[i].nxt)
if (e[i].to!=fa) {
if (sons[e[i].to].size()==2)
son2.push(e[i].to);
else if (sons[e[i].to].size()==1)
son1.push(e[i].to);
}
if (son2.size()%2==1 && son1.size() == 0 && son2.size()!=1) {
int x1 = son2.front();son2.pop();
int x2 = son2.front();son2.pop();
int x3 = son2.front();son2.pop();
anss[anscnt+1][0] = sons[x1].front();sons[x1].pop();
anss[anscnt+1][1] = sons[x2].front();sons[x2].pop();
anss[anscnt+2][0] = sons[x1].front();sons[x1].pop();
anss[anscnt+2][1] = sons[x3].front();sons[x3].pop();
son1.push(x2);
son1.push(x3);
anscnt+=2;
}
while (son2.size()>=2) {
int x1 = son2.front();son2.pop();
int x2 = son2.front();son2.pop();
anss[anscnt+1][0] = sons[x1].front();sons[x1].pop();
anss[anscnt+1][1] = sons[x2].front();sons[x2].pop();
//anss[anscnt+2][0] = sons[x1].front();sons[x1].pop();
//anss[anscnt+2][1] = sons[x2].front();sons[x2].pop();
son1.push(x1);
son1.push(x2);
anscnt++;
}
while (son2.size()*2 + son1.size() > 2) {
if (son2.size()) {
int x1 = son2.front();son2.pop();
int x2 = son1.front();son1.pop();
anss[anscnt+1][0] = sons[x1].front();sons[x1].pop();
anss[anscnt+1][1] = sons[x2].front();sons[x2].pop();
son1.push(x1);
anscnt++;
} else {
int x1 = son1.front();son1.pop();
int x2 = son1.front();son1.pop();
anss[anscnt+1][0] = sons[x1].front();sons[x1].pop();
anss[anscnt+1][1] = sons[x2].front();sons[x2].pop();
anscnt++;
}
}
for (int i = head[x];i;i=e[i].nxt)
if (e[i].to!=fa) {
while (sons[e[i].to].size())
{
int tmp = sons[e[i].to].front();
sons[x].push(tmp);
sons[e[i].to].pop();
}
}
}
int main()
{
int n;
scanf("%d",&n);
//printf("%d\n",n);
int x,y;
for (int i = 1;i< n;i++)
{
scanf("%d%d",&x,&y);
//printf("%d %d\n",x,y);
add(x,y);
du[x]++;du[y]++;
}
if (n==1) {
printf("1\n1 1\n");
return 0;
}
if (n==2) {
printf("1\n1 2\n");
return 0;
}
for (int i = 1;i<= n;i++)
if (du[i]!=1)
{
dfs(i,0);
if (sons[i].size()==2)
{
anscnt++;
anss[anscnt][0] = sons[i].front();
sons[i].pop();
anss[anscnt][1] = sons[i].front();
}
else {
anscnt++;
anss[anscnt][0] = sons[i].front();
anss[anscnt][1] = i;
}
break;
}
printf("%d\n",anscnt);
for (int i = 1;i<= anscnt;i++)
printf("%d %d\n",anss[i][0],anss[i][1]);
return 0;
}
\\
==== D - Duration ====
签到题,把两个时间都换算成到 $0:0:0$ 的秒数,相减取绝对值。
#include
#define ALL(x) (x).begin(),(x).end()
#define ll long long
#define ull unsigned long long
#define pii_ pair
#define mp_ make_pair
#define pb push_back
#define fi first
#define se second
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define show1(a) cout<<#a<<" = "<
\\
==== E - Exclusive OR ====
如果可以猜到结论/想到结论/打表观察可以发现对于$20\leq i$的$ans_i=ans_{i-2}$
那么就可以对于$i\leq 20$的时候fwt计算结果,其余的递推即可。
#include
using namespace std;
const int N = 1<<18;
const int M = 2e5+5;
typedef long long ll;
int n,nans[N],one[N],ans[M];
void FWT(int *src) {
for (int sz = 2;sz <= N;sz<<=1) {
int step = sz >> 1;
for (int i = 0;i< N;i+=sz) {
for (int j = i;j < i+step;j++) {
int a = src[j],b = src[j+step];
src[j] = a+b;
src[j+step] = a-b;
}
}
}
}
void IFWT(int *src) {
for (int sz = 2;sz <= N;sz<<=1) {
int step = sz >> 1;
for (int i = 0;i < N;i+=sz) {
for (int j = i;j> 1;
src[j+step] = (a-b)>>1;
}
}
}
}
int main()
{
scanf("%d",&n);
int x;
for (int i = 1;i<= n;i++) {
scanf("%d",&x);
one[x]=1;
nans[x]=1;
if (x > ans[1]) ans[1] = x;
}
FWT(one);
for (int i = 2;i<= 20;i++) {
FWT(nans);
for (int j = 0;j < N;j++)
nans[j] = nans[j]*one[j];
IFWT(nans);
for (int j = 0;j < N;j++)
if (nans[j])
nans[j] = 1;
for (int j = N-1;j>=0;j--)
if (nans[j]) {
ans[i] = j;
break;
}
}
for (int i = 21;i<= n;i++)
ans[i] = ans[i-2];
for (int i = 1;i<= n;i++)
printf("%d ",ans[i]);
}
\\
==== F - Fake Maxpooling ====
$n\times m$矩阵$a[i][j]=\gcd(i,j)$求所有$k\times k$子矩阵最大值的和
先对某一维单调队列(对于每行$k$列最大值缩为一格),之后另一维也单调队列。。$O(nm)$,gcd这个暴力要多个$\log n$,有点慢,但卡过去了,其实是应该记忆化
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include
#define ll long long
using namespace std;
const int N=5e3+10;
int a[N][N],b[N][N];
int head,tail,q[N*10];
inline int gcd(int a,int b){return b?gcd(b,a%b):a;}
inline int lcm(int a,int b){return a*b/gcd(a,b);}
int main()
{
int n,m,k;
scanf("%d%d%d",&n,&m,&k);
ll res=0;
for(int i=1;i<=n;i++)
{
head=1,tail=0;
for(int j=1;j<=m;j++)
{
a[i][j]=lcm(i,j);
while(tail>=head&&j-q[head]>=k)head++;
while(tail>=head&&a[i][j]>a[i][q[tail]])tail--;
q[++tail]=j;
if(j>=k)b[i][j-k+1]=a[i][q[head]];
}
}
for(int j=1;j<=m-k+1;j++)
{
head=1,tail=0;
for(int i=1;i<=n;i++)
{
while(tail>=head&&i-q[head]>=k)head++;
while(tail>=head&&b[i][j]>b[q[tail]][j])tail--;
q[++tail]=i;
if(i>=k)res+=b[q[head]][j];
}
}
printf("%lld\n",res);
return 0;
}
\\
==== G - Greater and Greater ====
给两个序列 $A$ 和 $B$,求 $A$ 的所有长度为 $|B|$ 的子串中,每一位都对应大于 $B$ 的每一位的个数。
用 $\text{bitset}$ 压位,先预处理出 $S$ ,其中 $S[i][j]=1$ 表示 $A[i]>=B[j]$,并维护 $A$ 的长度为 $|B|$ 的子串 $a$ 的前缀与 $B$ 的后缀的大小关系,考虑从后往前 $\text{dp}$。具体来说就是,$cur[i]=1$ 表示 $a$ 的 $m-i$ 前缀每一位都对应大于等于 $B$ 的 $m-i$ 后缀,从后往前滚动,每次向左移一位,把 $m-1$ 置为 $1$,再与 $S[i]$ 按位与,统计 $cur[0]=1$ 的个数就是答案。
#include
#define ALL(x) (x).begin(),(x).end()
#define ll long long
#define ull unsigned long long
#define pii_ pair
#define mp_ make_pair
#define pb push_back
#define fi first
#define se second
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define show1(a) cout<<#a<<" = "< S[40001],cur;
int main()
{
fastio();
cin>>n>>m;
rep(i,0,n-1) cin>>a[i];
rep(i,0,m-1) {cin>>b[i];c[i] = b[i];id[i]=i;}
sort(c,c+m);
sort(id,id+m,[](int x,int y){return b[x]0) S[i] |= S[i-1];
S[i][id[i]] = 1;
}
//rep(i,0,m-1) {rep(j,0,m-1) cout<>= 1;
cur[m-1] = 1;
if(pos==-1) cur &= S[m];
else cur &= S[pos];
ans += cur[0];
}
cout<
\\
==== H - Happy Triangle ====
题意:维护一个multiset,三种操作:添加删除查询。查询是查询对于x,multiset中是否存在两条边可以和其组成三角形。
分两种情况讨论,第一种情况是询问的边是最大的边,第二种情况是非最大边。对于最大边的情况可以发现找小于x的最大的两条边即可,这一部分直接用multiset就能维护。对于非最大边时,假设能组成三角形的另外两条边是a和b,那么一定有$x < b$且$b-a < x$,可以发现对于一个b,肯定是选择前驱作为a时$b-a$最小,所以我们需要做的就是写一个数据结构,使得可以维护当前点减去前驱点的后缀最小值。仝multiset+离散化+线段树可以做到这一操作。
#include
using namespace std;
const int N = 2e5+5;
const int INF = 2e9+5;
int tr[N<<2],id[N];
struct Node {
int op,val;
}qur[N];
void Build(int p,int l,int r) {
if (l==r) {
tr[p] = INF;
return ;
}
int mid = (l+r)>>1;
Build(p<<1,l,mid);
Build(p<<1|1,mid+1,r);
tr[p] = min(tr[p<<1],tr[p<<1|1]);
}
void Update(int p,int l,int r,int a,int b) {
if (l==r) {
tr[p] = b;
return ;
}
int mid = (l+r)>>1;
if (a <= mid) Update(p<<1,l,mid,a,b);
else Update(p<<1|1,mid+1,r,a,b);
tr[p] = min(tr[p<<1],tr[p<<1|1]);
}
int Getans(int p,int l,int r,int a,int b) {
if (l>=a&&r<=b) {
return tr[p];
}
int mid = (l+r)>>1;
int ans = INF;
if (a<=mid) ans = min(ans,Getans(p<<1,l,mid,a,b));
if (b>mid) ans = min(ans,Getans(p<<1|1,mid+1,r,a,b));
return ans;
}
multiset MS;
int c[N];
int main()
{
MS.insert(-1);MS.insert(-1);MS.insert(INF);
int q;
scanf("%d",&q);
for (int i = 1;i<= q;i++) {
scanf("%d%d",&qur[i].op,&qur[i].val);
id[i] = qur[i].val;
}
sort(id+1,id+q+1);
int cnt = unique(id+1,id+q+1)-id-1;
Build(1,1,cnt);
for (int i = 1;i<= q;i++) {
int x = qur[i].val;
int pos = lower_bound(id+1,id+cnt+1,x)-id;
if (qur[i].op == 1) {
MS.insert(x);
c[pos]++;
if (c[pos] == 2) {
Update(1,1,cnt,pos,0);
} else if (c[pos] == 1) {
Update(1,1,cnt,pos,(*MS.upper_bound(x))-x);
auto it = MS.lower_bound(x);
it--;
int pre = *it;
int prepos = lower_bound(id+1,id+cnt+1,pre)-id;
if (c[prepos] == 1)Update(1,1,cnt,prepos,x-pre);
}
} else if(qur[i].op == 2) {
MS.erase(MS.find(x));
c[pos]--;
if (c[pos] == 1) Update(1,1,cnt,pos,(*MS.upper_bound(x))-x);
else if (c[pos] == 0) {
Update(1,1,cnt,pos,INF);
auto it = MS.lower_bound(x);
it--;
int pre = *it;
int prepos = lower_bound(id+1,id+cnt+1,pre)-id;
if (c[prepos] == 1)Update(1,1,cnt,prepos,(*MS.lower_bound(x))-pre);
}
} else {
bool flag = false;
auto it = MS.lower_bound(x);
int t1 = *it;
int t2 = *(--it);
int t3 = *(--it);
if (t2+t3>x || t2+x>t1)flag = true;
if (Getans(1,1,cnt,pos,cnt) < x) flag = true;
if (flag)printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
\\
==== I - Interval ====
开局你有一个区间$[1,n]$,区间可以收缩和扩张,$[l,r]$和$[l+1,r]$、$[l,r]$和$[l,r-1]$间可以来回转换,但是到$l=r$时就不能再变了,所以不希望看到这种局面。给出$m$个限制,$l~r~dir~c$当$dir=L$时表示花费$c$可以限制$[l,r],[l+1,r]$之间的转换,$dir=R$时表示花费$c$可以限制$[l,r],[l,r-1]$之间的转换。问能够限制区间不可能变为$l=r$的最小花费。
{{:2020-2021:teams:wangzai_milk:grid_graph.png?300|}}
我们把区间看作坐标,显然这是一个从$(1,n)$到$y=x$的最小割。和狼抓兔子(BZOJ1001)比较像,网格图转对偶图求最短路即最小割,$O(n^2\log n)$
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include
#define pb push_back
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f3f3f
#define pii pair
#define idx(x,y) ((x-1)*(n-1)+y)
#define mp make_pair
const int N=505;
int n,m,s,t,head[N*N],tot=0,done[N*N];
ll dis[N*N];
struct Node
{
int nxt,to;ll w;
Node(int nxt=0,int to=0,ll w=0):nxt(nxt),to(to),w(w){}
}edges[N*N*10];
struct Node1
{
int u;ll d;
Node1(int u=0,ll d=0):u(u),d(d){}
bool operator < (const Node1 &a) const {return d>a.d;}
};
void addedge(int u,int v,ll w)
{
edges[++tot]=Node(head[u],v,w);
head[u]=tot;
}
ll dij()
{
priority_queueq;
memset(dis,0x3f,sizeof(dis));
dis[s]=0,q.push(Node1(s,dis[s]));
while(!q.empty())
{
int u=q.top().u;q.pop();
if(done[u])continue;
done[u]=1;
for(int i=head[u];~i;i=edges[i].nxt)
{
int v=edges[i].to;
if(dis[v]>dis[u]+edges[i].w)
{
dis[v]=dis[u]+edges[i].w;
q.push(Node1(v,dis[v]));
}
}
}
return dis[t]==INF?-1:dis[t];
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d%d",&n,&m);
s=0,t=(n-1)*(n-1)+1;
for(int i=1;i<=m;i++)
{
int l,r,c;char dir[2];
scanf("%d%d%s%d",&l,&r,dir,&c);
int id1,id2;
if(dir[0]=='L')
{
id1=idx(l,r),id2=idx(l,r-1);
if(r==n)id1=s;
}
else
{
id2=idx(l-1,r-1),id1=idx(l,r-1);
if(l==1)id2=t;
}
addedge(id1,id2,c),addedge(id2,id1,c);
}
printf("%lld\n",dij());
return 0;
}
\\
==== J - Just Shuffle ====
给一个置换 $A$ 和大质数 $k$,求置换 $P$ 使得 $P^k = A$。
求出 $A$ 的每个循环节,因为 $k$ 是个大质数,所以 $P$ 每个循环节作 $k$ 次方都不会分裂,于是 $P$ 的循环节和 $A$ 的循环节是一样的,只是向右平移了 $k$ 对循环节长度的逆元个长度。
#include
#define ALL(x) (x).begin(),(x).end()
#define ll long long
#define ull unsigned long long
#define pii_ pair
#define mp_ make_pair
#define pb push_back
#define fi first
#define se second
#define pg PermutationGroup
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define show1(a) cout<<#a<<" = "< pos;
int u = a[i],len = 1;pos.pb(i);
while(u!=i){
len++;
vis[u] = 1;pos.pb(u);
u = a[u];
}
ll x,y;
extgcd(k,len,x,y);
x = (x%len+len)%len;
rep(i,1,len){
ans[pos[i-1]] = pos[(i-1+x)%len];
}
}
rep(i,1,n) printf("%d ",ans[i]);
return 0;
}
\\
==== K - Keyboard Free ====
三个半径分别为$r_1,r_2,r_3$的同心圆上分别取一点构成三角形,求面积期望,保留一位小数。(懂了,保留一位小数就是增量乱搞qaq)
{{:2020-2021:teams:wangzai_milk:r123.png?300|}}
暴力积分有点难,涉及到负面积之类的,所以掺杂一点乱搞成分比较好。第一维位置固定,第二维增量乱搞,第三维easy积分。
第一维取点$G$,第二维在$[0,\pi]$间均匀枚举一定量的角度得$E$,第三位积分算圆周到$EG$直线的距离即可得三角形面积。
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include
using namespace std;
const double pi=acos(-1.0);
double r1,r2,r3;
double sqr(double x){return x*x;}
double f(double j)
{
double EG=sqrt(sqr(r1)+sqr(r2)-2*cos(j)*r1*r2);
double AGE=acos((sqr(r1)+sqr(EG)-sqr(r2))/2/r1/EG);
double alp=asin(r1*sin(AGE)/r3);
return r3*(alp*sin(alp)+cos(alp))*EG/pi;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%lf%lf%lf",&r1,&r2,&r3);
if(r1>r2)swap(r1,r2);
if(r2>r3)swap(r2,r3);
if(r1>r2)swap(r1,r2);
double res=0,j=pi/10000.0;
for(int i=0;i<5000;i++,j+=pi/5000.0)res+=f(j);
printf("%.1lf\n",res/5000);
}
return 0;
}
\\
===== 比赛总结与反思 =====
补完了!感觉这场题蛮正常的(m(就是隐约觉得都可做,然而没做对)。这种场的要义是不是不能陷入僵局quq(卡题乃正常场之大忌),好几道没怎么看的题后来发现可能更值得入手?啊下周加油。——Zars19
C题一开始想错了耽误了一些时间,想到了对的实现还很艰难呜呜呜呜该多写难题代码了。FWT这个有点可惜,感觉要多做一些FWT题了。 ——Infinity37
和上一场一样都是一个题卡了几个小时?(以后要试着跳出来。 ——_wzx27