Potyczki Algorytmiczne 2010
Trial Round:
Rectangles
枚举子矩阵的长和宽,乘以对应的子矩形数。
时间复杂度$O(nm)$。
#include<cstdio>
int n,m,p,i,j,ans;
int main(){
scanf("%d%d%d",&n,&m,&p);
for(i=1;i<=n;i++)for(j=1;j<=m;j++)if(2*(i+j)>=p)ans+=(n-i+1)*(m-j+1);
printf("%d",ans);
}
Round 1:
Orienteering [B]
将环倍长,破环成链。递推预处理出每个点往前一路数值不下降/不上升能到达哪里,即可$O(1)$判断每个起点是否合法。
时间复杂度$O(n)$。
#include<cstdio>
const int N=200005;
int n,i,a[N],f[N],g[N];
int main(){
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&a[i]),a[i+n]=a[i];
for(i=1;i<=n+n;i++){
f[i]=i;
if(i>1&&a[i-1]<=a[i])f[i]=f[i-1];
g[i]=i;
if(i>1&&a[i-1]>=a[i])g[i]=g[i-1];
if(f[i]<=i-n+1||g[i]<=i-n+1)return puts("TAK"),0;
}
puts("NIE");
}
Round 2:
Mushrooms [B]
最优方案一定是从起点出发往右走到某个点$x$,然后在$x$与$x-1$这两个位置左右横跳,枚举右端点$x$后计算对应方案的值,更新答案。
时间复杂度$O(n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1000005,BUF=8100000;
int n,i,t,a[N];ll s[N],ans;
char Buf[BUF],*buf=Buf;
inline void read(int&a){for(a=0;*buf<48;buf++);while(*buf>47)a=a*10+*buf++-48;}
inline ll cal(ll a,ll b,ll k){
ll ret=(a+b)*(k/2);
if(k%2)ret+=a;
return ret;
}
int main(){
fread(Buf,1,BUF,stdin),read(n),read(t);
for(i=1;i<=n&&i<=t+1;i++){
read(a[i]),s[i]=s[i-1]+a[i];
ans=max(ans,s[i]+cal(a[i-1],a[i],t-i+1));
}
printf("%lld",ans);
}
Coins [A]
令字符'O'的值为$1$,字符'R'的值为$-k$,问题转化为求最长的值的总和为$0$的子串。
求出前缀和后,将所有下标按前缀和分组,每组里面选取最小的下标$+1$作为左端点、最大的下标作为右端点,更新答案。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000005;
int n,k,i,j,q[N],ans;long long s[N];char a[N];
inline bool cmp(int x,int y){return s[x]==s[y]?x<y:s[x]<s[y];}
int main(){
scanf("%d%d%s",&n,&k,a+1);
for(i=1;i<=n;i++){
if(a[i]=='O')s[i]=s[i-1]+1;else s[i]=s[i-1]-k;
q[i]=i;
}
sort(q,q+n+1,cmp);
for(i=j=0;i<=n;i=j){
for(j=i;j<=n&&s[q[i]]==s[q[j]];j++);
ans=max(ans,q[j-1]-q[i]);
}
printf("%d",ans);
}
Round 3:
Fragments [A]
对于每个给定的区间$[l,r]$,从高位到低位递归枚举数字的每一位,在递归的过程中,如果发现当前已经脱离了上下界$[l,r]$的限制且之前不全是$0$,那么后面这些位可以任填,此时令前面已经确定的部分对应的数字串为$A$,后面未确定的部分的数字串为$B$,询问串为$S$,则有如下三种情况:
- $S$完全出现在$B$中:对应的贡献只和$B$的长度有关,可以很方便地计算出答案。
- $S$完全出现在$A$中:暴力枚举$A$的每个长度为$|S|$的子串,更新对应询问串的出现次数。
- $A$的某个后缀是$S$的前缀:对所有询问串建立Trie,暴力枚举$A$的每个后缀,那么满足条件的$S$在Trie上对应一个子树,更新子树的答案即可。
令$l$为数字的位数,$k$为字符集大小,则时间复杂度为$O(nl^2k+ml)$,需要很精细地实现使得时间复杂度不多$l$。
此外,由于内存限制很紧,需要将Trie的节点数压缩至$O(m)$,且需要将询问分批来做以减小$m$的大小。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5005,M=250005,K=19,E=M*2;
ll po[K+5];
int n,m,_m,i,j,cur,cnt[K+5];
char l[N][K],r[N][K],e[M][K+1],len[M];
ll ans[M],full[K+5];
int tot;
char d[E];//每个点到根的字符数
int who[E];//每个点到根是哪个串
int son[E][10];short mask[E];char low[1027];
int loc[M];
ll f[E];
int at[K+5][K+5];
inline void read(char*a){
ll t;
scanf("%lld",&t);
for(int i=K-1;~i;i--)a[i]=t%10,t/=10;
}
inline void ins(int o){
static char s[K+5];
scanf("%s",s+1);
int l=strlen(s+1),i;
len[o]=l;
for(i=1;i<=l;i++){
s[i]-='0';
e[o][i]=s[i];
}
int x=1,y=1,z;
for(i=1;i<=l;i++){
char w=s[i];
if(x==y){
if(!son[x][w]){
son[x][w]=++tot;
d[tot]=l;
who[tot]=o;
loc[o]=tot;
mask[x]|=1<<w;
return;
}
y=son[x][w];
z=w;
}
if(e[who[y]][i]==w){
if(i==d[y]){
x=y;
if(i==l)loc[o]=y;
}else if(i==l){
tot++;
d[tot]=l;
who[tot]=o;
loc[o]=tot;
son[tot][e[who[y]][i+1]]=y;
son[x][z]=tot;
mask[tot]|=1<<e[who[y]][i+1];
}
continue;
}
tot++;
d[tot]=i-1;
who[tot]=who[y];
son[tot][e[who[y]][i]]=y;
son[x][z]=tot;
mask[tot]|=1<<e[who[y]][i];
x=y=tot;
i--;
}
}
inline int go(int x,int y,int z){
if(!x)return 0;
if(d[x]>=y){
if(e[who[x]][y]==z)return x;
return 0;
}
return son[x][z];
}
ll dfs(int o,int x,int el,int er,int fir){
if(x==K)return 1;
if(!el&&!er&&fir<K){
cnt[K-x]++;
return po[K-x];
}
ll ret=0;
for(int i=0;i<10;i++){
int nel=el,ner=er,nfir=fir;
if(el){
if(i<l[o][x])continue;
if(i!=l[o][x])nel=0;
}
if(er){
if(i>r[o][x])continue;
if(i!=r[o][x])ner=0;
}
if(fir==K&&i)nfir=x;
for(int j=nfir;j<=x;j++){
if(j==x)at[x+1][j]=go(1,1,i);
else at[x+1][j]=go(at[x][j],x-j+1,i);
}
ll tmp=dfs(o,x+1,nel,ner,nfir);
for(int j=fir;j<x;j++){
int u=at[x][j];
if(!u)continue;
if(d[u]==x-j)f[u]+=tmp;
}
ret+=tmp;
}
return ret;
}
void dfs2(int o,int x,int el,int er,int fir,int u){
if(((!el&&!er)||(x==K))&&fir<K){
/*
A=[0..x)B=[x..K)
for each suffix T (can be non-empty) of A
*/
if(u<=1)return;
if(fir>cur)return;
f[u]++;
return;
}
for(int i=0;i<10;i++){
int nel=el,ner=er,nfir=fir,nu=u;
if(el){
if(i<l[o][x])continue;
if(i!=l[o][x])nel=0;
}
if(er){
if(i>r[o][x])continue;
if(i!=r[o][x])ner=0;
}
if(fir==K&&i)nfir=x;
if(nfir<K&&x>=cur)nu=go(u,x-cur+1,i);
dfs2(o,x+1,nel,ner,nfir,nu);
}
}
void dfs3(int x){
for(int i=mask[x];i;i-=i&-i){
int y=son[x][low[i]];
f[y]+=f[x];
dfs3(y);
}
}
inline ll cal(int x){
ll ret=0;
for(int i=x;i<=K;i++)ret+=po[i-x]*cnt[i]*(i-x+1);
return ret;
}
int main(){
scanf("%d%d",&n,&_m);
for(po[0]=i=1;i<=K;i++)po[i]=po[i-1]*10;
for(i=1;i<=n;i++){
read(l[i]);
read(r[i]);
}
for(i=1;i<1024;i++)low[i]=__builtin_ctz(i);
while(_m){
m=min(_m,M-5);
_m-=m;
tot=1;
for(i=1;i<=m;i++)ins(i);
for(i=1;i<=n;i++)dfs(i,0,1,1,K);
for(i=1;i<=m;i++)ans[i]=f[loc[i]];
for(cur=0;cur<K;cur++){
for(i=1;i<=tot;i++)f[i]=0;
for(i=1;i<=n;i++)dfs2(i,0,1,1,K,1);
dfs3(1);
for(i=1;i<=m;i++){
j=K-cur-len[i];
if(j>=0)ans[i]+=f[loc[i]]*po[j];
}
}
for(i=1;i<=K;i++)full[i]=cal(i);
for(i=1;i<=m;i++)printf("%lld\n",ans[i]+full[len[i]]);
for(i=0;i<=K;i++)cnt[i]=0;
for(i=0;i<=tot;i++){
mask[i]=f[i]=0;
for(j=0;j<10;j++)son[i][j]=0;
}
}
}
Squared Words [B]
$ans=\max_i\left\{LCS(S[1..i],S[i+1..n])\right\}$,共$O(n)$次LCS询问。使用ALCS在$O(n^2)$时间内完成预处理,$O(n)$时间内回答每个询问。
总时间复杂度$O(n^2)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1005;
int n,i,j,f[N][N],g[N][N],ans;char s[N];
inline int ask(int o,int l,int r){
int len=r-l+1,i,j,k,ret=0;
for(i=1;i<=len;i++){
j=r-i+1;
if(j>=f[o+1][j+1]){
ret++;
k=f[o+1][j+1]-1;
if(k>=l)ret--;
}
}
return ret;
}
int main(){
scanf("%d%s",&n,s);
for(i=1;i<=n;i++)f[0][i]=i;
for(i=1;i<=n;i++)for(j=1;j<=n;j++){
if(s[i-1]==s[j-1]){
f[i][j]=g[i][j-1];
g[i][j]=f[i-1][j];
}else{
f[i][j]=max(f[i-1][j],g[i][j-1]);
g[i][j]=min(g[i][j-1],f[i-1][j]);
}
}
for(i=0;i<n-1;i++)ans=max(ans,ask(i,i+1,n-1));
printf("%d",n-ans*2);
}
Round 4:
Evacuation [A]
$1$到$n$的边数不超过$3$的路径有如下三种:
- $1\rightarrow n$:这种边必须删除。
- $1\rightarrow i\rightarrow n$:边$1\rightarrow i$和边$i\rightarrow n$至少要删掉一条。
- $1\rightarrow i\rightarrow j\rightarrow n$:边$1\rightarrow i$、边$i\rightarrow j$和边$j\rightarrow n$至少要删掉一条,不难发现删掉中间的边$i\rightarrow j$不优,因此可以视作边$1\rightarrow i$和边$j\rightarrow n$至少要删掉一条。
建立左右各$n$个点的二分图,将第二类和第三类中涉及的边作为连源点/汇点的边加入,并将对应的二选一限制作为无穷边加入,则问题转化为求最小割,即二分图最大匹配,使用bitset优化的匈牙利算法即可。
时间复杂度$O(\frac{n^3}{w})$。
#include<cstdio>
typedef unsigned int U;
const int N=1005,M=(N>>5)+5;
int n,m,l,r,all,i,j,x,y,ans,idl[N],idr[N],who[N];
bool g[N][N];U f[N][M],v[M];
inline void flip(U v[],int x){v[x>>5]^=1U<<(x&31);}
bool find(int x){
for(int i=0;i<=all;i++)while(1){
U t=v[i]&f[x][i];
if(!t)break;
int y=i<<5|__builtin_ctz(t);
flip(v,y);
if(!who[y]||find(who[y]))return who[y]=x,1;
}
return 0;
}
int main(){
scanf("%d%d",&n,&m);
while(m--){
scanf("%d%d",&x,&y);
if(x==1&&y==n){ans++;continue;}
g[x][y]=1;
}
for(i=1;i<=n;i++){
if(g[1][i]&&g[i][n])idl[i]=idr[i]=1;
for(j=1;j<=n;j++)if(g[1][i]&&g[i][j]&&g[j][n])idl[i]=idr[j]=1;
}
for(i=1;i<=n;i++){
if(idl[i])idl[i]=++l;
if(idr[i])idr[i]=++r;
}
for(i=1;i<=n;i++){
if(g[1][i]&&g[i][n])flip(f[idl[i]],idr[i]);
for(j=1;j<=n;j++)if(g[1][i]&&g[i][j]&&g[j][n])flip(f[idl[i]],idr[j]);
}
all=r>>5;
for(i=1;i<=l;i++){
for(j=0;j<=all;j++)v[j]=~0U;
if(find(i))ans++;
}
printf("%d",ans);
}
Map [B]
从左往右考虑每个点,维护考虑过的点中$y$坐标的最小值和最大值,即可$O(1)$判断每个点左上角和左下角是否有点,右上角和右下角的判断同理。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000005;
int n,d,i,j,l,r,ans,f[N];
struct P{int x,y,p;}a[N];
inline bool cmp(const P&a,const P&b){return a.x<b.x;}
void gao(){
l=d,r=0;
for(i=1;i<=n;i=j){
for(j=i;j<=n&&a[i].x==a[j].x;j++){
if(l<a[j].y)f[a[j].p]++;
if(r>a[j].y)f[a[j].p]++;
}
for(j=i;j<=n&&a[i].x==a[j].x;j++){
l=min(l,a[j].y);
r=max(r,a[j].y);
}
}
}
int main(){
scanf("%d%d",&n,&d);
for(i=1;i<=n;i++){
scanf("%d%d",&a[i].x,&a[i].y);
a[i].p=i;
}
sort(a+1,a+n+1,cmp);
gao();
reverse(a+1,a+n+1);
gao();
for(i=1;i<=n;i++)if(f[i]==4)ans++;
printf("%d",ans);
}
Round 5:
The Goat [A]
首先在$O(n^2\log n)$时间内求出$area[i]$表示被恰好$i$个圆覆盖部分的面积,则每个位置在$k$轮中至少被覆盖一次的概率为$1-(1-\frac{i}{n})^k$,将概率乘以$area[i]$后贡献给答案即可。
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=1010;
const double eps=1e-9;
const double PI=acos(-1.0);
int sgn(double x){
if(x>eps)return 1;
if(x<-eps)return -1;
return 0;
}
struct P{
double x,y;
P(){}
P(double _x,double _y){x=_x,y=_y;}
P operator+(const P&b)const{return P(x+b.x,y+b.y);}
P operator-(const P&b)const{return P(x-b.x,y-b.y);}
P operator*(double b)const{return P(x*b,y*b);}
P operator/(double b)const{return P(x/b,y/b);}
double det(const P&b)const{return x*b.y-y*b.x;}
P rot90()const{return P(-y,x);}
P unit(){return *this/abs();}
double abs(){return hypot(x,y);}
};
struct Circle{
P o;double r;
bool contain(const Circle&v,const int&c)const{
return sgn(r-(o-v.o).abs()-v.r)>c;
}
bool disjuct(const Circle&v,const int&c)const{
return sgn((o-v.o).abs()-r-v.r)>c;
}
};
bool isCC(Circle a,Circle b,P&p1,P&p2){
if(a.contain(b,0)||b.contain(a,0)||a.disjuct(b,0))return 0;
double s1=(a.o-b.o).abs();
double s2=(a.r*a.r-b.r*b.r)/s1;
double aa=(s1+s2)/2,bb=(s1-s2)/2;
P mm=(b.o-a.o)*(aa/(aa+bb))+a.o;
double h=sqrt(max(0.0,a.r*a.r-aa*aa));
P vv=(b.o-a.o).unit().rot90()*h;
p1=mm+vv,p2=mm-vv;
return 1;
}
struct EV{
P p;double ang;int add;
EV(){}
EV(const P&_p,double _ang,int _add){p=_p,ang=_ang,add=_add;}
bool operator<(const EV&a)const{return ang<a.ang;}
}eve[N*2];
int E,cnt,C,K,L,i,j;Circle c[N];
bool g[N][N],overlap[N][N];
double Area[N],ans;
int cX[N],cY[N],cR[N];
bool contain(int i,int j){
return (sgn(c[i].r-c[j].r)>0||sgn(c[i].r-c[j].r)==0&&i<j)&&c[i].contain(c[j],-1);
}
int main(){
scanf("%d%d%d",&C,&K,&L);
for(i=0;i<C;i++){
scanf("%d%d",&cX[i],&cY[i]);
cR[i]=L;
c[i].o=P(cX[i],cY[i]);
c[i].r=cR[i];
}
for(i=0;i<=C;i++)Area[i]=0;
for(i=0;i<C;i++)for(j=0;j<C;j++)overlap[i][j]=contain(i,j);
for(i=0;i<C;i++)for(j=0;j<C;j++)g[i][j]=!(overlap[i][j]||overlap[j][i]||c[i].disjuct(c[j],-1));
for(i=0;i<C;i++){
E=0;cnt=1;
for(j=0;j<C;j++)if(j!=i&&overlap[j][i])cnt++;
for(j=0;j<C;j++)if(i!=j&&g[i][j]){
P aa,bb;
isCC(c[i],c[j],aa,bb);
double A=atan2(aa.y-c[i].o.y,aa.x-c[i].o.x);
double B=atan2(bb.y-c[i].o.y,bb.x-c[i].o.x);
eve[E++]=EV(bb,B,1);
eve[E++]=EV(aa,A,-1);
if(B>A)cnt++;
}
if(E==0)Area[cnt]+=PI*c[i].r*c[i].r;
else{
sort(eve,eve+E);
eve[E]=eve[0];
for(j=0;j<E;j++){
cnt+=eve[j].add;
Area[cnt]+=eve[j].p.det(eve[j+1].p)*0.5;
double theta=eve[j+1].ang-eve[j].ang;
if(theta<0)theta+=PI*2;
Area[cnt]+=theta*c[i].r*c[i].r*0.5-sin(theta)*c[i].r*c[i].r*0.5;
}
}
}
for(i=1;i<=C;i++)ans+=(Area[i]-Area[i+1])*(1-pow(1-1.0*i/C,K));
return printf("%.15f",ans),0;
}
Map 2 [B]
将横坐标离散化,对于离散化后的每一段连续的横坐标,预处理出它左边/右边所有点的纵坐标的最小值和最大值,则这一段的纵坐标能取的范围是对应区间的交,将交集大小乘以这一段横坐标对应的横坐标数,贡献给答案即可。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000005;
int n,d,i,j,l,r,sufl[N],sufr[N];long long ans;
struct P{int x,y;}a[N];
inline bool cmp(const P&a,const P&b){return a.x<b.x;}
inline int cal(int L,int R){return max(min(R,r)-max(L,l)-1,0);}
int main(){
scanf("%d%d",&n,&d);
for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y);
sort(a+1,a+n+1,cmp);
l=d,r=0;
for(i=n+1;i;i--){
if(i<=n)l=min(l,a[i].y),r=max(r,a[i].y);
sufl[i]=l,sufr[i]=r;
}
l=d,r=0;
for(i=1;i<=n;i=j){
ans+=1LL*(a[i].x-a[i-1].x-1)*cal(sufl[i],sufr[i]);
for(j=i;j<=n&&a[i].x==a[j].x;j++);
ans+=cal(sufl[j],sufr[j]);
for(j=i;j<=n&&a[i].x==a[j].x;j++)l=min(l,a[j].y),r=max(r,a[j].y);
}
printf("%lld",ans);
}
Termites [A]
两个人拿走的石子堆数是定值$cnt$,石子数总和是定值$sum$,考虑求出先手总和$-$后手总和的值$dif$,那么就能推出两人各自的总和。
对于相邻的三个元素$A,B,C$,如果$A\leq B$且$B\geq C$,那么$A$和$C$一定属于同一个人,$B$一定属于另一个人,可以将这三个数合并为一个数$A-B+C$。不断迭代直至无法继续合并数字。
迭代完毕后,对于最左侧或者最右侧贴墙的部分,令贴墙的数为$A$,$A$旁边的数为$B$,那么如果$A\geq B$,则这两个数一定是最后由两人轮流拿走,可以将它们消去,把墙推进两格,并给$dif$加上$(-1)^{cnt}\times(B-A)$。不断迭代直至无法继续消除数字。
如上处理完毕后,可以保证先手每次一定能取到全局最大值,因此将所有数混在一起排序,从大到小贪心取即可。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
typedef long long ll;
const int N=1000010;
int n,m,all,i,j,a[N],cnt;ll st[N],q[N],sum,dif;
inline void ext(int x){
st[++cnt]=x;
while(cnt>2&&st[cnt-2]<=st[cnt-1]&&st[cnt-1]>=st[cnt]){
st[cnt-2]+=st[cnt]-st[cnt-1];
cnt-=2;
}
}
inline void solve(int l,int r){
int i;
cnt=0;
if(r==n)for(i=r;i>=l;i--)ext(a[i]);
else for(i=l;i<=r;i++)ext(a[i]);
if(l==1||r==n){
for(i=1;i<cnt;i+=2)if(st[i]>=st[i+1]){
if(all&1)dif-=st[i+1]-st[i];
else dif+=st[i+1]-st[i];
}else break;
for(;i<=cnt;i++)q[++m]=st[i];
}else for(i=1;i<=cnt;i++)q[++m]=st[i];
}
int main(){
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&a[i]),sum+=a[i],all+=!!a[i];
for(i=1;i<=n;)if(a[i]){
for(j=i;j<=n&&a[j];j++);
solve(i,j-1);
i=j;
}else i++;
std::sort(q+1,q+m+1);
for(i=m,j=0;i;i--,j^=1)if(!j)dif+=q[i];else dif-=q[i];
printf("%lld %lld",(sum+dif)/2,(sum-dif)/2);
}
Round 6:
Byton Tree [B]
按照可采摘时间区间的右端点$r$从小到大依次考虑每个叶子。
假设当前考虑到了叶子$x$,暴力找到$x$最高的祖先$y$,满足$y$子树内$l$的最大值不超过$r[x]$,并标记沿途经过的点。
若一个点已经被标记过则说明$x$已经被摘走,否则此时需要在$y$处进行一次采摘。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000005;
int n,m,i,l[N],r[N],f[N],q[N],ans;bool vis[N];
inline bool cmp(int x,int y){return r[x]<r[y];}
void init(int y){
int k,x=++n;
f[x]=y;
scanf("%d",&k);
if(!k){
scanf("%d%d",&l[x],&r[x]);
q[++m]=x;
}
while(k--)init(x);
}
inline void gao(int x){
int o=r[x];
vis[x]=1;
while(f[x]&&l[f[x]]<=o){
x=f[x];
if(vis[x])return;
vis[x]=1;
}
ans++;
}
int main(){
init(0);
for(i=n;i;i--)l[f[i]]=max(l[f[i]],l[i]);
sort(q+1,q+m+1,cmp);
for(i=1;i<=m;i++)gao(q[i]);
printf("%d",ans);
}
Firm [B]
按深度离线后,使用树状数组支持单点修改以及子树点数查询。
时间复杂度$O(n\log n)$。
#include<cstdio>
#include<vector>
using namespace std;
typedef vector<int>V;
#define rep(x) for(V::iterator it=x.begin();it!=x.end();it++)
const int N=100005,M=N*2;
int m,i,x,y,e[N][2],ans[N],d[N],st[N],en[N],dfn,f[N];
V g[N],G[M];char op[N][5];
void dfs(int x){
st[x]=++dfn;
rep(g[x])dfs(*it);
en[x]=dfn;
}
inline void add(int x,int p){for(;x<=dfn;x+=x&-x)f[x]+=p;}
inline int ask(int x){int t=0;for(;x;x-=x&-x)t+=f[x];return t;}
int main(){
scanf("%d",&m);
for(i=1;i<=m;i++){
scanf("%s%d%d",op[i],&x,&y);
e[i][0]=x,e[i][1]=y;
if(op[i][0]=='Z'){
d[x]=d[y]+1;
g[y].push_back(x);
G[d[x]].push_back(x);
}else{
G[d[x]+y+1].push_back(-i);
}
}
dfs(1);
for(i=0;i<M;i++){
rep(G[i]){
x=*it;
if(x>0)add(st[x],1);
else{
y=e[-x][0];
ans[-x]=ask(en[y])-ask(st[y]-1);
}
}
rep(G[i])if(*it>0)add(st[*it],-1);
}
for(i=1;i<=m;i++)if(op[i][0]=='P')printf("%d\n",ans[i]);
}
Planning the Roadworks [A]
Kosaraju求出SCC后,缩点得到一个DAG。
对于DAG上的某条边$x\rightarrow y$,如果去掉这条边后$x$仍然能到达$y$,那么可以删掉这条边,即要判断能否通过拓扑序介于$x$和$y$之间的点从$x$间接到达$y$。按拓扑序DP出$can[x][y]$表示$x$能否到$y$,对于每个点按照另一个端点的拓扑序枚举边进行转移,假设现在枚举到了边$x\rightarrow y$,则拓扑序介于它们之间的点已经更新过$can[x][y]$,利用$can[x][y]$即可判断出去掉边$x\rightarrow y$后$x$是否仍然能到达$y$。使用bitset优化,时间复杂度$O(\frac{nm}{w})$。
对于SCC内部的边,由于Kosaraju只用到了不超过$2n$条边,留下这些边后对于每条边暴力检查去掉它后还能否从$x$到达$y$即可,时间复杂度$O(n^2)$。
总时间复杂度为$O(\frac{nm}{w}+n^2)$。
#include<cstdio>
#include<bitset>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int>P;
typedef vector<P>V;
#define rep(it,v) for(V::iterator it=v.begin();it!=v.end();it++)
const int N=5005,M=100005;
int n,m,cnt,i,j,x,y,e[M][2],q[N],t,f[N],pool[N],tot,ans;
V g[N],h[N];
bool vis[N],used[M],del[M];
bitset<N>can[N];
void dfs1(int x){
vis[x]=1;
rep(it,g[x])if(!vis[it->first])used[it->second]=1,dfs1(it->first);
q[++t]=x;
}
void dfs2(int x){
vis[x]=0,f[x]=cnt;
pool[++tot]=x;
rep(it,h[x])if(vis[it->first])used[it->second]=1,dfs2(it->first);
}
void dfs3(int x){
vis[x]=1;
rep(it,g[x])if(!vis[it->first]&&!del[it->second])dfs3(it->first);
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++){
scanf("%d%d",&x,&y);
e[i][0]=x,e[i][1]=y;
g[x].push_back(P(y,i));
h[y].push_back(P(x,i));
}
for(i=1;i<=n;i++)if(!vis[i])dfs1(i);
for(i=n;i;i--)if(vis[q[i]])cnt++,dfs2(q[i]);
for(i=1;i<=n;i++)g[i].clear();
for(i=1;i<=n;i++)rep(it,h[pool[i]]){
x=f[pool[i]],y=f[it->first];
if(x==y)continue;
g[y].push_back(P(x,it->second));
}
for(i=1;i<=cnt;i++)can[i][i]=1;
for(i=cnt;i;i--)rep(it,g[i]){
if(can[i][it->first])del[it->second]=1;
else can[i]|=can[it->first];
}
for(i=1;i<=n;i++)g[i].clear();
for(i=1;i<=m;i++){
x=e[i][0],y=e[i][1];
if(f[x]!=f[y])continue;
if(!used[i]){del[i]=1;continue;}
g[x].push_back(P(y,i));
}
for(i=1;i<=m;i++){
x=e[i][0],y=e[i][1];
if(f[x]!=f[y])continue;
if(!used[i])continue;
del[i]=1;
for(j=1;j<=n;j++)vis[j]=0;
dfs3(x);
if(!vis[y])del[i]=0;
}
for(i=1;i<=m;i++)if(del[i])ans++;
printf("%d\n",ans);
for(i=1;i<=m;i++)if(del[i])printf("%d\n",i);
}
Riddle [A]
注意到“每个集合恰好选择一个点”可以放宽成“每个集合最多选择一个点”,对于最后求出的方案里,如果某个集合没选点,任选一个就好了。
考虑2-SAT建图,有两类边:
- 对于每条给定的边$(u,v)$:如果不选$u$就必须选$v$,如果不选$v$就必须选$u$。
- 对于每个集合:如果选了一个点就不能选其它所有点。
第二类边不能直接建图,但是在Kosaraju算法中DFS图的时候,每个点$x$和$x$所在集合内除了$x$之外的所有点都连了一条第二类边,需要用一个数据结构跳过那些已经搜过的且不是$x$的点。用一个支持双端pop的队列维护就可以了,如果这个集合不是只剩$x$没搜过,那么两端至少可以消费一个点。
时间复杂度$O(n+m+k)$。
#include<cstdio>
const int N=2000010,M=1000010,BUF=25000000;
char Buf[BUF],*buf=Buf;
int n,m,K,o,i,j,x,y,S[M],T[M],st[M],en[M],pool[M],tot,at[M];
int e[M][2],g[N],v[N],nxt[N],ed;
int q[N],t,f[N];
bool vis[N];
inline void read(int&a){for(a=0;*buf<48;buf++);while(*buf>47)a=a*10+*buf++-48;}
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;}
void dfs1(int x){
if(vis[x])return;
vis[x]=1;
for(int i=g[x];i;i=nxt[i])dfs1(v[i]);
if(x>n)while(S[at[x-n]]<=T[at[x-n]]){
if(pool[S[at[x-n]]]!=x-n)dfs1(pool[S[at[x-n]]++]);
else if(pool[T[at[x-n]]]!=x-n)dfs1(pool[T[at[x-n]]--]);
else break;
}
q[++t]=x;
}
void dfs2(int x){
if(!vis[x])return;
vis[x]=0,f[x]=o;
for(int i=g[x];i;i=nxt[i])dfs2(v[i]);
if(x<=n)while(S[at[x]]<=T[at[x]]){
if(pool[S[at[x]]]!=x)dfs2(pool[S[at[x]]++]+n);
else if(pool[T[at[x]]]!=x)dfs2(pool[T[at[x]]--]+n);
else break;
}
}
int main(){
fread(Buf,1,BUF,stdin);read(n),read(m),read(K);
for(i=1;i<=m;i++){
read(x),read(y);
e[i][0]=x,e[i][1]=y;
add(x,y+n),add(y,x+n);
}
for(i=1;i<=K;i++){
read(y);
st[i]=tot+1;
while(y--){
read(x);
at[x]=i;
pool[++tot]=x;
}
en[i]=tot;
}
for(i=1;i<=K;i++)S[i]=st[i],T[i]=en[i];
for(i=1;i<=n+n;i++)if(!vis[i])dfs1(i);
for(ed=0,i=1;i<=n+n;i++)g[i]=0;
for(i=1;i<=m;i++){
x=e[i][0],y=e[i][1];
add(y+n,x),add(x+n,y);
}
for(i=1;i<=K;i++)S[i]=st[i],T[i]=en[i];
for(i=t;i;i--)if(vis[q[i]])o++,dfs2(q[i]);
for(i=1;i<=n;i++)if(f[i]==f[i+n])return puts("NIE"),0;
puts("TAK");
for(i=1;i<=K;i++)st[i]=pool[st[i]];
for(i=1;i<=n;i++)if(f[i]<f[i+n])st[at[i]]=i;
for(i=1;i<=K;i++)printf("%d ",st[i]);
return 0;
}
Trial Finals:
Variable Subsequences
设$f_i$表示以$i$为结尾的合法子序列数,枚举上一个点转移,由于只有一类转移不合法,可以使用总和减去那一类的贡献来进行$O(1)$转移。
时间复杂度$O(n)$。
#include<cstdio>
const int N=500005,P=1000000007;
int n,s,t,x,f[N];
int main(){
s=1;
scanf("%d",&n);
while(n--){
scanf("%d",&x);
t=(s-f[x]+P)%P;
f[x]=(f[x]+t)%P;
s=(s+t)%P;
}
printf("%d",(s+P-1)%P);
}
Rectangles 2
同Rectangles。
Finals:
Sweets
假设最后分成的三组糖果分别为$A$个、$B$个、$C$个,其中$A\leq B\leq C$,则要最小化$C-A$。
将$n$箱糖果分成前一半和后一半,对于每一半暴力枚举出所有$O(3^{\frac{n}{2}})$个可能的分组方案,对于一个方案记录$a=B-A,b=C-B$,那么需要在前一半方案中找到一个方案$i$,在后一半方案中找到一个方案$j$,满足$A\leq B\leq C$,即$B-A\geq 0$且$C-B\geq 0$,也就是:
- $a_i+a_j\geq 0$
- $b_i+b_j\geq 0$
并最小化$C-A$,即$(C-B)+(B-A)=a_i+a_j+b_i+b_j=(a_i+b_i)+(a_j+b_j)$。
将所有方案按$a$排序,枚举前一半的方案$i$,双指针出满足$a_i+a_j\geq 0$的$j$的范围,按$b$建立树状数组,查询区间$a_j+b_j$的最大值,更新答案。
时间复杂度$O(n3^{\frac{n}{2}})$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=27,M=535005;
const ll inf=1LL<<60;
int n,m,ca,cb,i,j,w[N];ll ans,tmp,c[M],f[M];
struct P{ll x,y;}a[M],b[M];
inline bool cmp(const P&a,const P&b){return a.x<b.x;}
void dfsl(int o,ll x,ll y){
if(o>m){
a[++ca].x=-x;
a[ca].y=-y;
return;
}
dfsl(o+1,x-w[o],y);
dfsl(o+1,x+w[o],y-w[o]);
dfsl(o+1,x,y+w[o]);
}
void dfsr(int o,ll x,ll y){
if(o>n){
b[++cb].x=x;
b[cb].y=y;
return;
}
dfsr(o+1,x-w[o],y);
dfsr(o+1,x+w[o],y-w[o]);
dfsr(o+1,x,y+w[o]);
}
inline void up(ll&a,ll b){a>b?(a=b):0;}
inline void add(int x,ll p){for(;x<=ca;x+=x&-x)up(f[x],p);}
inline ll ask(int x){ll t=inf;for(;x;x-=x&-x)up(t,f[x]);return t;}
int main(){
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&w[i]);
m=n/2;
dfsl(1,0,0);
dfsr(m+1,0,0);
ans=inf;
for(i=1;i<=ca;i++)c[i]=a[i].y,f[i]=inf;
sort(c+1,c+ca+1);
sort(a+1,a+ca+1,cmp);
sort(b+1,b+cb+1,cmp);
for(i=j=1;i<=cb;i++){
while(j<=ca&&a[j].x<=b[i].x){
add(lower_bound(c+1,c+ca+1,a[j].y)-c,-a[j].x-a[j].y);
j++;
}
up(ans,ask(upper_bound(c+1,c+ca+1,b[i].y)-c-1)+b[i].x+b[i].y);
}
printf("%lld",ans);
}
Acyclic Decomposition
DFS判环,若不存在环则答案为$1$。
否则可以构造答案为$2$的一组解:第一组包含所有满足$u<v$的边,第二组包含所有满足$u>v$的边。
时间复杂度$O(n+m)$。
#include<cstdio>
const int N=100005;
int n,m,i,x,y,ca,cb,a[N],b[N],g[N],v[N],nxt[N],vis[N],in[N];
bool dfs(int x){
if(vis[x])return 0;
vis[x]=in[x]=1;
for(int i=g[x];i;i=nxt[i]){
if(in[v[i]])return 1;
if(dfs(v[i]))return 1;
}
return in[x]=0;
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++){
scanf("%d%d",&x,&y);
if(x<y)a[++ca]=i;else b[++cb]=i;
v[i]=y,nxt[i]=g[x],g[x]=i;
}
for(i=1;i<=n;i++)if(dfs(i)){
puts("2");
for(printf("%d",ca),i=1;i<=ca;i++)printf(" %d",a[i]);puts("");
for(printf("%d",cb),i=1;i<=cb;i++)printf(" %d",b[i]);puts("");
return 0;
}
puts("1");
printf("%d",m);
for(i=1;i<=m;i++)printf(" %d",i);
}
Divisors
Pollard-Rho分解质因数,计算结果唯一当且仅当每个质因数的计算结果唯一。
对于每个质因数,将所有变量分别设为最小和最大值,计算出式子的结果,判断是否相同即可。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int C=2730,S=5,N=600005;
int Case,m,cnt,i,j,k,tot,l[N],r[N],f[N],g[N];
ll n,q[100],v[N];
char s[2111111];
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
inline ll mul(ll a,ll b,ll n){
a%=n,b%=n;
if(n<=1100000000)return a*b%n;
return(a*b-(ll)(a/(long double)n*b+1e-8)*n+n)%n;
}
inline ll pow(ll a,ll b,ll n){
ll d=1;
a%=n;
while(b){
if(b&1)d=mul(d,a,n);
a=mul(a,a,n);
b>>=1;
}
return d;
}
inline bool check(ll a,ll n){
ll m=n-1,x,y;int i,j=0;
while(!(m&1))m>>=1,j++;
x=pow(a,m,n);
for(i=1;i<=j;x=y,i++){
y=pow(x,2,n);
if((y==1)&&(x!=1)&&(x!=n-1))return 1;
}
return y!=1;
}
inline bool miller_rabin(int times,ll n){
ll a;
if(n==1)return 0;
if(n==2)return 1;
if(!(n&1))return 0;
while(times--)if(check(rand()%(n-1)+1,n))return 0;
return 1;
}
inline ll pollard_rho(ll n,int c){
ll i=1,k=2,x=rand()%n,y=x,d;
while(1){
i++,x=(mul(x,x,n)+c)%n,d=gcd(y-x,n);
if(d>1&&d<n)return d;
if(y==x)return n;
if(i==k)y=x,k<<=1;
}
}
void findfac(ll n,int c){
if(n==1)return;
if(miller_rabin(S,n)){
q[++cnt]=n;
return;
}
ll m=n;
while(m==n)m=pollard_rho(n,c--);
findfac(m,c),findfac(n/m,c);
}
int build(){
int x=++tot;
l[x]=r[x]=v[x]=0;
scanf("%s",s);
if(s[0]>='a'&&s[0]<='z')return x;
if(s[0]>='0'&&s[0]<='9'){
int len=strlen(s);
for(int i=0;i<len;i++)(v[x]*=10)+=s[i]-'0';
return x;
}
if(s[2]=='D')v[x]=1;else v[x]=2;
l[x]=build();
r[x]=build();
return x;
}
inline bool solve(){
scanf("%lld",&n);
tot=0;
build();
cnt=0;
findfac(n,C);
sort(q+1,q+cnt+1);
for(i=1;i<=cnt;i=j){
for(j=i;j<=cnt&&q[i]==q[j];j++);
ll A=q[i];int B=j-i;
for(k=tot;k;k--){
if(l[k]){
if(v[k]==1){//gcd
f[k]=min(f[l[k]],f[r[k]]);
g[k]=min(g[l[k]],g[r[k]]);
}else{
f[k]=max(f[l[k]],f[r[k]]);
g[k]=max(g[l[k]],g[r[k]]);
}
}else if(v[k]){
ll C=v[k];int D=0;
while(C%A==0)C/=A,D++;
f[k]=g[k]=D;
}else{
f[k]=0;
g[k]=B;
}
}
if(f[1]!=g[1])return 0;
}
return 1;
}
int main(){
scanf("%d",&Case);
while(Case--)puts(solve()?"TAK":"NIE");
}
Byteball Match
枚举每个球队$S$,判断$S$能否成为冠军。
由于每个球队都还没结束比赛,因此只要在剩下比赛里都比对方领先$+\infty$个球即可达到最高预期分数,那么只需要判断剩下的比赛能否合理分配$2$分给双方使得每个队伍的分数都不超过$S$的最高预期分数。
将剩下每个球队看作点,点权表示$S$的最高预期分数减去该球队已有的分数。
将剩下还未进行的比赛看作边,那么类似于Hall定理,这有解当且仅当任何一个点集都满足点权和$\geq $诱导子图边数$\times2$,即$\min(点权和-诱导子图边数\times 2)\geq 0$,需要找到一个点集$V$最小化点权和$-$诱导子图边数$\times 2$。
注意到诱导子图边数$\times 2=V$中所有点的度数之和$-$横跨$V$内外的边数,因此问题等价于最小化"每个点的(点权$-$度数)之和$+$横跨$V$内外的边数",对该问题建立最小割模型:
- 源点到每个点的边的边权为$n+$点权$-$度数,割掉这条边表示将这个点选入$V$。
- 每个点到汇点的边边权为$n$,割掉这条边表示不将这个点选入$V$。
- 每条边正边反边的代价都是$1$,表示如果一方选了且另一方没选,则要割掉这条边。
利用HLPP算法$O(n^3)$求最大流判断最小割是否$\geq (n-1)\times n$即可,总时间复杂度$O(n^4)$。
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
using namespace std;
namespace HLPP{
const int N = 105, M = 120000, INF = 0x3f3f3f3f;
int n, m, s, t;
struct E{
int nex, t, v;
};
E e[M * 2 + 1];
int h[N + 1], cnt;
inline void add_path(int f, int t, int v) { e[++cnt] = (E){h[f], t, v}, h[f] = cnt; }
inline void add_flow(int f, int t, int v) {
add_path(f, t, v);
add_path(t, f, 0);
}
int ht[N + 1], ex[N + 1],
gap[N]; // 高度; 超额流; gap 优化 gap[i] 为高度为 i 的节点的数量
stack<int> B[N]; // 桶 B[i] 中记录所有 ht[v]==i 的v
int level; // 溢出节点的最高高度
inline void init(int _n,int _s,int _t){
n=_n,s=_s,t=_t;
cnt=1;
level=0;
memset(h, 0, sizeof(h));
memset(ht, 0, sizeof(ht));
memset(ex, 0, sizeof(ex));
memset(gap, 0, sizeof(gap));
for(int i=0;i<N;i++)while(!B[i].empty())B[i].pop();
}
inline int push(int u) { // 尽可能通过能够推送的边推送超额流
bool init = u == s; // 是否在初始化
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t, &w = e[i].v;
if (!w || init == false && ht[u] != ht[v] + 1) // 初始化时不考虑高度差为1
continue;
int k = init ? w : min(w, ex[u]);
// 取到剩余容量和超额流的最小值,初始化时可以使源的溢出量为负数。
if (v != s && v != t && !ex[v]) B[ht[v]].push(v), level = max(level, ht[v]);
ex[u] -= k, ex[v] += k, e[i].v -= k, e[i ^ 1].v += k; // push
if (!ex[u]) return 0; // 如果已经推送完就返回
}
return 1;
}
inline void relabel(int u) { // 重贴标签(高度)
ht[u] = INF;
for (int i = h[u]; i; i = e[i].nex)
if (e[i].v) ht[u] = min(ht[u], ht[e[i].t]);
if (++ht[u] < n) { // 只处理高度小于 n 的节点
B[ht[u]].push(u);
level = max(level, ht[u]);
++gap[ht[u]]; // 新的高度,更新 gap
}
}
inline bool bfs_init() {
memset(ht, 0x3f, sizeof(ht));
static int q[N + 5];
int head = 0, tail = 0;
ht[q[0] = t] = 0;
while (head <= tail) { // 反向 BFS, 遇到没有访问过的结点就入队
int u = q[head++];
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (e[i ^ 1].v && ht[v] > ht[u] + 1) ht[v] = ht[u] + 1, q[++tail] = v;
}
}
return ht[s] != INF; // 如果图不连通,返回 0
}
// 选出当前高度最大的节点之一, 如果已经没有溢出节点返回 0
inline int select() {
while (B[level].size() == 0 && level > -1) level--;
return level == -1 ? 0 : B[level].top();
}
inline int hlpp() { // 返回最大流
if (!bfs_init()) return 0; // 图不连通
memset(gap, 0, sizeof(gap));
for (int i = 1; i <= n; i++)
if (ht[i] != INF) gap[ht[i]]++; // 初始化 gap
ht[s] = n;
push(s); // 初始化预流
int u;
while ((u = select())) {
B[level].pop();
if (push(u)) { // 仍然溢出
if (!--gap[ht[u]])
for (int i = 1; i <= n; i++)
if (i != s && i != t && ht[i] > ht[u] && ht[i] < n + 1)
ht[i] = n + 1; // 这里重贴成 n+1 的节点都不是溢出节点
relabel(u);
}
}
return ex[t];
}
}
const int N=105;
int n,m,i,x,y,A,B,f[N],v[N][N],mx,id[N],deg[N];
inline bool check(int S){
int i,j,now=f[S],cnt=2;
for(i=1;i<=n;i++)if(!v[S][i])now+=2;
if(now<mx)return 0;
for(i=1;i<=n;i++)if(i!=S)id[i]=++cnt,deg[i]=0;
HLPP::init(n+1,1,2);
for(i=1;i<=n;i++)for(j=1;j<=n;j++)if(!v[i][j]&&i!=S&&j!=S){
HLPP::add_flow(id[i],id[j],1);
deg[i]++;
}
for(i=1;i<=n;i++)if(i!=S){
HLPP::add_flow(1,id[i],N+now-f[i]-deg[i]);
HLPP::add_flow(id[i],2,N);
}
return HLPP::hlpp()>=(n-1)*N;
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)v[i][i]=1;
while(m--){
scanf("%d%d%d%d",&x,&y,&A,&B);
v[x][y]=v[y][x]=1;
if(A==B)f[x]++,f[y]++;
if(A>B)f[x]+=2;
if(A<B)f[y]+=2;
}
mx=n-1;
for(i=1;i<=n;i++)mx=max(mx,f[i]);
for(i=1;i<=n;i++)if(check(i))printf("%d ",i);
}
Army Training
选择最左边的点$O$作为原点,将所有点按$O$极角排序,求出每个点的排名$rk[i]$。
对于任意一个三角形$OAB$ ($rk[A]<rk[B]$),预处理出严格在它内部的点数,即$rk$在$(rk[A],rk[B])$之间且在直线$AB$一侧的点数。枚举$A$后将所有点按$A$极角排序,转一圈的同时用树状数组维护$rk$。
对于每个询问,首先由预处理的三角形信息乘以叉积的正负得到一个初步的答案。这里存在一些顶点$P$满足射线$OP$与多边形交点为奇数,没有被正确地减掉。
枚举多边形上相邻的三个点$X,Y,Z$,根据$X,Y,Z$的相对关系分类讨论判断$Y$是否是这样多算的点即可。
时间复杂度$O(n^2\log n+\sum k)$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1005;
int n,m,q,i,j,x,y,z,k,ans,rk[N],f[N],w[N],g[N][N],id[N];bool vis[N];
struct P{
int x,y,p;
P(){}
P(int _x,int _y){x=_x,y=_y;}
P operator-(const P&b)const{return P(x-b.x,y-b.y);}
int sgn()const{return x?x>0:y>0;}
}a[N],b[N],c[N<<1],pivot;
inline ll cross(const P&a,const P&b){return 1LL*a.x*b.y-1LL*a.y*b.x;}
inline bool cmp(const P&a,const P&b){return cross(a-pivot,b-pivot)>0;}
inline bool cmp2(const P&a,const P&b){
if(a.sgn()!=b.sgn())return a.sgn()<b.sgn();
return cross(a,b)>0;
}
inline void ins(int x){for(;x<n;x+=x&-x)f[x]++;}
inline int ask(int x){int t=0;for(;x;x-=x&-x)t+=f[x];return t;}
inline bool check(int x,int y,int z){
if(!rk[y])return 0;
if(!rk[x])return rk[z]>rk[y];
if(!rk[z])return rk[x]<rk[y];
if(rk[x]<rk[y]&&rk[y]<rk[z])return 1;
if((rk[x]<rk[y]&&rk[z]<rk[y])||(rk[x]>rk[y]&&rk[z]>rk[y]))return cross(a[y]-a[x],a[z]-a[y])>0;
return 0;
}
int main(){
scanf("%d%d",&n,&q);
for(i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y),a[i].p=i;
for(pivot=a[1],i=2;i<=n;i++)if(a[i].x<pivot.x)pivot=a[i];
for(i=1;i<=n;i++)if(a[i].p!=pivot.p)b[++m]=a[i];
sort(b+1,b+n,cmp);
for(i=1;i<n;i++)rk[b[i].p]=i;
for(i=1;i<n-1;i++){
for(m=0,j=i+1;j<n;j++){
c[++m]=b[j];
c[m].x-=b[i].x;
c[m].y-=b[i].y;
}
for(j=1;j<=m;j++){
c[j+m]=c[j];
c[j+m].x*=-1;
c[j+m].y*=-1;
c[j+m].p*=-1;
}
sort(c+1,c+m+m+1,cmp2);
for(j=1;j<=n;j++)f[j]=w[j]=vis[j]=0;
for(j=1;j<=m+m;j++){
k=c[j].p;
if(k>0){
ins(rk[k]);
w[k]-=ask(rk[k]-1);
vis[k]=1;
}else{
k*=-1;
w[k]+=ask(rk[k]-1);
if(!vis[k])w[k]+=rk[k]-i-1;
}
}
for(j=i+1;j<n;j++){
k=w[b[j].p];
if(cross(a[b[i].p]-pivot,a[b[j].p]-pivot)>0)k*=-1;
g[b[i].p][b[j].p]=k;
g[b[j].p][b[i].p]=-k;
}
}
while(q--){
scanf("%d",&m);
for(i=0;i<m;i++)scanf("%d",&id[i]);
for(ans=i=0;i<m;i++){
x=id[i],y=id[(i+1)%m],z=id[(i+2)%m];
ans+=g[x][y];
if(check(x,y,z))ans--;
}
printf("%d\n",ans);
}
}
Blindfold Nim
全$0$时先手胜率为$0$,否则最优策略一定是从上限最大的那一堆拿走一个石子。
假设上限最大的那一堆的石子数在$[0,k]$之间随机,则先手在这一轮的操作有$\frac{k}{k+1}$的概率合法,然后交换先后手,因此令之后子问题的先手胜率为$nxt$,则答案为$\frac{k}{k+1}(1-nxt)$,可以倒推求出原始局面的先手胜率。
时间复杂度$O(n+\sum a)$。
#include<cstdio>
const int N=1000000;
int n,i,c[N+5],f[N+5];long double ans;
int main(){
scanf("%d",&n);
while(n--)scanf("%d",&i),c[i]++;
for(i=N,n=0;i;i--)while(c[i])c[i]--,c[i-1]++,f[++n]=i;
for(i=n;i;i--)ans=(1-ans)*f[i]/(f[i]+1);
printf("%.15f",(double)ans);
}
Termites 2
按时间顺序从前往后考虑每条边,看作$n$个独立点然后依次加入每条边将它们逐渐连通,那么每个连通块的游戏是独立的,且对于每个连通块,只有一个点可以留下来。
因此对于$x$点所在的连通块,设$a[x]$表示玩家1可以保证最后留下来的点集,设$b[x]$表示玩家2可以保证最后留下来的点集。
按时间顺序从前往后考虑到边$(u,v)$时,不妨设现在轮到了玩家1,令合并$u,v$后的连通块对应的信息为$a',b'$:
- 如果$a[u]$不能保证留下$u$且$a[v]$不能保证留下$v$,那么游戏结束。
- 如果$a[u]$能保证留下$u$且$a[v]$能保证留下$v$,那么如果吃掉$u$,则可以保全$a[v]$;如果吃掉$v$,则可以保全$a[u]$,而对手保全不了任何点,因此$a'=a[u]\ or\ a[v]$,$b'=NULL$。
- 如果$a[u]$能保证留下$u$但是$a[v]$不能保证留下$v$,那么只能吃掉$u$和$v$,保全$a[v]$和$b[v]-v$,因此$a'=a[v]$,$b'=b[v]-v$。
用数组记录每个点是否能被两个玩家保全,并查集维护连通块,建二叉树表示集合的or关系。在清除一个集合时,暴力DFS对应子树修正数组的值即可。
时间复杂度$O(n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=500005;
int n,m,i,x,y,X,Y,f[N<<1],a[N],b[N],ga[N<<1][2],gb[N<<1][2];
int F(int x){return f[x]==x?x:f[x]=F(f[x]);}
void dfsa(int x){
if(!x)return;
dfsa(ga[x][0]);
dfsa(ga[x][1]);
if(x<=n)a[x]=0;
}
void dfsb(int x){
if(!x)return;
dfsb(gb[x][0]);
dfsb(gb[x][1]);
if(x<=n)b[x]=0;
}
int main(){
scanf("%d",&n);
m=n;
for(i=1;i<=n;i++)a[i]=b[i]=f[i]=i;
for(i=1;i<n;i++){
scanf("%d%d",&x,&y);
++m;
f[m]=m;
if(i&1){
if(!a[x]&&!a[y])return printf("%d",i),0;
if(a[x]&&a[y]){
X=F(x),Y=F(y);
//a(M)=a(X) or a(Y), b(M)=NULL
ga[m][0]=X;
ga[m][1]=Y;
dfsb(X);
dfsb(Y);
}else{
if(!a[x])swap(x,y);
X=F(x),Y=F(y);
//a(M)=a(Y), b(M)=b(Y)-y
b[y]=0;
ga[m][0]=gb[m][0]=Y;
dfsa(X);
dfsb(X);
}
}else{
if(!b[x]&&!b[y])return printf("%d",i),0;
if(b[x]&&b[y]){
X=F(x),Y=F(y);
//b(M)=b(X) or b(Y), a(M)=NULL
gb[m][0]=X;
gb[m][1]=Y;
dfsa(X);
dfsa(Y);
}else{
if(!b[x])swap(x,y);
X=F(x),Y=F(y);
//b(M)=b(Y), a(M)=a(Y)-y
a[y]=0;
ga[m][0]=gb[m][0]=Y;
dfsa(X);
dfsb(X);
}
}
f[X]=f[Y]=m;
}
puts("-1");
}

浙公网安备 33010602011771号