A. Salty Fish
假設要偷走所有的蘋果,那么現在需要放棄一些蘋果,並且黑掉一些監控相機以保證能偷走沒有放棄的蘋果,我們需要最小化放棄的蘋果的收益之和加上黑掉相機支付的代價的總和。
考慮最小割建圖:
- 源點$S$向每個監控相機連邊,割掉這條邊的代價為黑掉它的代價,割掉這條邊表示黑掉這個監控相機。
- 每個節點向匯點$T$連邊,割掉這條邊的代價為這個點的蘋果數,割掉這條邊表示放棄這個節點的蘋果。
- 每個監控相機向其監控范圍內的所有節點連邊,割掉這條邊的代價為$+\infty$,表示不能破壞監控關系。
那么每條$S$到$T$的路徑都表示某個節點既沒有並放棄,又被一些相機監控着,這是不合法的。我們需要割掉代價之和最少的邊,使得$S$和$T$不連通,因此最終的答案就是所有節點的蘋果數量之和減去這個圖的最小割。
因為最小割=最大流,從葉子向根節點依次考慮每棵子樹,計算最大流。設$v[i][j]$表示$i$的子樹中離$i$距離為$j$的那些節點還能提供多少流量,那么考慮監控范圍最高點在$i$的每個監控相機,顯然應該優先接收距離較大的那些節點的流量。
用std::map存儲每個值非零的$v[i]$,可以在總計$O(m\log n)$的時間內求出最大流。至於$v[i]$的計算,可以由$i$的兒子的$v$啟發式合並而來。
注意到這是關於深度的一個啟發式合並,如果將這棵樹長鏈剖分,計算出每個點$x$子樹內離$x$距離最遠的點到$x$的距離$d[x]$,那么可以選擇將$d$最大的兒子的$v$繼承給$x$,然后將其它兒子的$v$暴力插入到$v[x]$中。因為每個點僅屬於一條長鏈,且一條長鏈只會在鏈頂位置作為短兒子被暴力合並一次,所以合並的時間復雜度為$O(n)$。
總時間復雜度為$O((n+m)\log n)$。
#include<cstdio>
#include<map>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=300010;
int Case,n,m,i,j,x,A,B,f[N],d[N],a[N],e[N][2],g[N],nxt[N];ll ans;map<int,ll>T[N];
inline void add(int x,int y){nxt[y]=g[x];g[x]=y;}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d%d",&n,&m);
ans=0;
for(i=1;i<=n;i++)g[i]=0,T[i].clear();
for(i=2;i<=n;i++)scanf("%d",&f[i]),d[i]=d[f[i]]+1;
for(i=1;i<=n;i++)scanf("%d",&a[i]),ans+=a[i];
for(i=1;i<=m;i++)scanf("%d%d%d",&x,&e[i][0],&e[i][1]),add(x,i);
for(i=n;i;i--){
T[i][d[i]]+=a[i];
for(j=g[i];j;j=nxt[j]){
A=d[i]+e[j][0],B=e[j][1];
while(B&&T[i].size()){
map<int,ll>::iterator it=T[i].upper_bound(A);
if(it==T[i].begin())break;
it--;
x=B<it->second?B:it->second;
B-=x;
it->second-=x;
ans-=x;
if(!it->second)T[i].erase(it);
}
}
if(i>1){
x=f[i];
if(T[x].size()<T[i].size())swap(T[x],T[i]);
for(map<int,ll>::iterator it=T[i].begin();it!=T[i].end();it++)if(it->second)T[x][it->first]+=it->second;
}
}
printf("%lld\n",ans);
}
}
B. Nonsense Time
考慮時間倒流,看作一個完整的排列按照一定順序依次刪除每個數,然后每次需要計算LIS的長度。
首先在$O(n\log n)$的時間內求出LIS,並找到一個LIS。當刪除$x$時,如果$x$不在之前找到的那個LIS中,那么顯然LIS的長度是不會變化的,否則暴力重新計算出新的LIS即可。
因為數據隨機,因此LIS的期望長度是$O(\sqrt{n})$,刪除的$x$位於LIS中的概率是$\frac{1}{\sqrt{n}}$,也就是說期望刪除$O(\sqrt{n})$個數才會修改LIS,那么LIS變化的次數不會很多。
期望時間復雜度為$O(n\sqrt{n}\log n)$。
#include<cstdio>
const int N=50010;
int Case,n,i,x,a[N],b[N],ans[N],pre[N],nxt[N],f[N],g[N],used[N],bit[N];
inline void up(int&a,int b){if(f[a]<f[b])a=b;}
inline void build(){
int i,j,k;
for(i=nxt[0];i<=n+1;i=nxt[i]){
used[i]=0;
k=0;
for(j=a[i];j;j-=j&-j)up(k,bit[j]);
f[i]=f[k]+1;
g[i]=k;
for(j=a[i];j<=n+2;j+=j&-j)up(bit[j],i);
}
for(i=nxt[0];i<=n+1;i=nxt[i])for(j=a[i];j<=n+2;j+=j&-j)bit[j]=0;
for(i=n+1;i;i=g[i])used[i]=1;
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&a[i]),a[i]++;
a[0]=1;
a[n+1]=n+2;
for(i=0;i<=n+1;i++)pre[i]=i-1,nxt[i]=i+1,bit[i]=used[i]=0;
bit[n+2]=0;
for(i=1;i<=n;i++)scanf("%d",&b[i]);
build();
for(i=n;i;i--){
ans[i]=f[n+1]-1;
x=b[i];
pre[nxt[x]]=pre[x];
nxt[pre[x]]=nxt[x];
if(used[x])build();
}
for(i=1;i<=n;i++)printf("%d%c",ans[i],i<n?' ':'\n');
}
}
C. Milk Candy
建立一張$n+1$個點的圖,點的編號為$0$到$n$,點$i$表示$s_i=x_1+x_2+\dots+x_i$。如果我們知道了$x_l+x_{l+1}+\dots+x_r$,那么我們就知道了$s_r-s_{l-1}$ 的值,在$l-1$和$r$之間連一條邊。如果這個圖是連通的,那么我們就能根據$s_0=0$推出所有$s$,從而推出所有$x$。問題轉化為從每個NPC手中恰好購買$k_i$條邊,使得這個圖連通,且代價之和最小。
從另外一個角度考慮這個問題:先購買所有邊,然后從每個NPC手中刪除不超過$c_i-k_i$條邊,總計刪除恰好$\sum(c_i-k_i)$條邊,使得剩下的圖仍然連通,且刪去的邊代價之和最大。由於刪去邊后圖連通等價於剩下的邊存在生成樹,生成樹是圖擬陣的基,所以這是圖擬陣的對偶擬陣$M_1$;而從每個邊集中選擇不超過若干條邊的條件,則是划分擬陣$M_2$。
所以我們的目標就是找到這兩個擬陣的交的大小為$\sum(c_i-k_i)$的權值和最大的獨立集,可以用擬陣交算法解決:
- 令初始解$I$為空集,即沒有邊被刪除。
- 每條邊作為有向圖中的一個點,並新建源點$S$和匯點$T$。
- 對於$x\notin I$的某條邊$x$,將$x$的點權設置為$w_x$,表示額外刪掉這條邊的代價。若$I\cup x$滿足$M_1$,則連邊$S\rightarrow x$;若$I\cup x$滿足$M_2$,則連邊$x\rightarrow T$。
- 對於$x\in I$的某條邊$x$,將$x$的點權設置為$-w_x$,表示取消刪除這條邊的代價。
- 對於$x\in I$的某條邊$x$以及$y\notin I$的某條邊$y$,若$I\setminus x\cup y$滿足$M_1$,則連邊$x\rightarrow y$;若$I\setminus x\cup y$滿足$M_2$,則連邊$y\rightarrow x$。
- 在構造出來的圖中SPFA找到$S$到$T$的最長路作為增廣路,將上面每條邊的選擇情況取反,得到新的解$I'$,此時$I'$的大小比$I$剛好大$1$。不斷重復構圖找增廣路直至$I$的大小為$\sum(c_i-k_i)$。
不妨認為$n,m,\sum c$同階,則一共$O(n)$次增廣,每次增廣建圖需要$O(n^3)$的時間,尋找增廣路需要$O(n^3)$的SPFA,總時間復雜度為$O(n^4)$。
#include<cstdio>
const int N=85,M=100000,inf=~0U>>1;
int Case,n,cnt,m,goal,have,num[N],lim[N],i,j;
int S,T,x,y,g[N],v[N<<1],nxt[N<<1],ed,vis[N];
int cost[N],col[N],use[N],ans;
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;}
void dfs(int x){
if(vis[x])return;
vis[x]=1;
for(int i=g[x];i;i=nxt[i])if(use[i>>1])dfs(v[i]);
}
inline bool check(){
int i;
for(i=0;i<=n;i++)vis[i]=0;
dfs(0);
for(i=0;i<=n;i++)if(!vis[i])return 0;
return 1;
}
inline bool check2(){
for(int i=1;i<=cnt;i++)if(num[i]<lim[i])return 0;
return 1;
}
namespace Matroid{
int g[N],v[M],nxt[M],ed,q[M],h,t,d[N],pre[N],w[N];bool in[N];
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];g[x]=ed;}
inline void ext(int x,int y,int z){
if(d[x]<=y)return;
d[x]=y;
pre[x]=z;
if(in[x])return;
q[++t]=x;
in[x]=1;
}
inline bool find(){
int i,j;
S=m+1,T=m+2;
for(ed=0,i=1;i<=T;i++)g[i]=0;
for(i=1;i<=m;i++)if(use[i]){
w[i]=-cost[i];
use[i]^=1;
num[col[i]]--;
if(check())add(S,i);
if(check2())add(i,T);
num[col[i]]++;
use[i]^=1;
}else w[i]=cost[i];
for(i=1;i<=m;i++)if(use[i])for(j=1;j<=m;j++)if(!use[j]){
use[i]^=1,use[j]^=1;
num[col[i]]--;num[col[j]]++;
if(check())add(j,i);
if(check2())add(i,j);
num[col[i]]++;num[col[j]]--;
use[i]^=1,use[j]^=1;
}
for(i=1;i<=T;i++)d[i]=inf,in[i]=0;
q[h=t=1]=S;
d[S]=0,in[S]=1;
while(h<=t){
x=q[h++];
for(i=g[x];i;i=nxt[i])ext(v[i],d[x]+w[v[i]],x);
in[x]=0;
}
if(d[T]==inf)return 0;
ans+=d[T];
while(pre[T]!=S){
T=pre[T];
if(use[T])num[col[T]]--;else num[col[T]]++;
use[T]^=1;
}
return 1;
}
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d%d",&n,&cnt);
m=goal=ans=0;
for(i=0;i<=n;i++)g[i]=0;
for(ed=i=1;i<=cnt;i++){
scanf("%d%d",&num[i],&lim[i]);
goal+=lim[i];
for(j=0;j<num[i];j++){
m++;
col[m]=i;
scanf("%d%d%d",&x,&y,&cost[m]);
add(x-1,y);
add(y,x-1);
use[m]=1;
ans+=cost[m];
}
}
if(!check()){
puts("-1");
continue;
}
have=m;
while(have>goal){
if(!Matroid::find())break;
have--;
}
if(have!=goal)ans=-1;
printf("%d\n",ans);
}
}
D. Speed Dog
問題等價於找到一堆$x_i(0\leq x_i\leq 1)$,使得下面式子的值最小:
\[
\max\left(\sum_{i=1}^n a_ix_i,\sum_{i=1}^n b_i(1-x_i)\right)
\]
因為
\[
\max\left(A,B\right)=\max_{0\leq k\leq 1}\left(kA+(1-k)B\right)
\]
所以
\begin{eqnarray*}
&&\max\left(\sum_{i=1}^n a_ix_i,\sum_{i=1}^n b_i(1-x_i)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)
\end{eqnarray*}
根據Minimax Theorem,有
\begin{eqnarray*}
&&\min_{0\leq x_1,x_2,\dots,x_n\leq 1}\left(\max_{0\leq k\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\min_{0\leq x_1,x_2,\dots,x_n\leq 1}\left(\sum_{i=1}^n ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n \min_{0\leq x_i\leq 1}\left(ka_ix_i+(1-k)b_i(1-x_i)\right)\right)\\
&=&\max_{0\leq k\leq 1}\left(\sum_{i=1}^n \min\left(ka_i,(1-k)b_i\right)\right)
\end{eqnarray*}
注意到對於固定的$i$來說,$\min\left(ka_i,(1-k)b_i\right)$關於$k$的函數是一個凸函數,而凸函數的和$f(k)$也為凸函數,因此可以通過三分這個$k$得到答案。
對於固定的$i$來說,當$ka_i=(1-k)b_i$,也就是$k=\frac{b_i}{a_i+b_i}$時取得極值,所以只有$O(n)$個這樣的$k$是有用的,只需要在它們之間三分,也因此避免了浮點數運算。注意這里需要對這些$k$進行去重,否則三分時可能會出現函數平台導致三分失敗。
現在剩下的問題就是如何快速得到答案。對於這些$k$建立一棵權值線段樹,將每個二元組$(a_i,b_i)$放在分界線$\frac{b_i}{a_i+b_i}$的位置上。線段樹每個節點維護對應區間內$a$的和、$b$的和以及區間左端點和右端點對應的$a$和$b$的和,插入一個新的二元組的時間復雜度為$O(\log n)$。
查詢最優解時,只需要從線段樹根節點開始,假設左子樹表示$[l,mid]$,右子樹表示$[mid+1,r]$,那么通過比較$f(mid)$和$f(mid+1)$的大小即可知道極值位於左子樹還是右子樹,通過線段樹維護的信息可以$O(1)$算出$f(mid)$和$f(mid+1)$的值。
時間復雜度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=250010,M=524305;
int Case,n,m,_,i,x,y,pos[N];ll sa[M],sb[M],la[M],lb[M],alla;
struct E{int x,y;}e[N];
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
struct Num{
ll u,d;
Num(){}Num(ll _u,ll _d){u=_u,d=_d;}
void write(){
ll z=gcd(u,d);
printf("%lld/%lld\n",u/z,d/z);
}
}q[N];
inline int cmp(const Num&a,const Num&b){
ll t=a.u*b.d-b.u*a.d;
if(t<0)return -1;
return t?1:0;
}
inline bool cmpn(const Num&a,const Num&b){return cmp(a,b)<0;}
inline int lower(int A,int B){
Num x(A,B);
int l=1,r=m,mid,t;
while(1){
mid=(l+r)>>1;
t=cmp(x,q[mid]);
if(!t)return mid;
if(t<0)r=mid-1;else l=mid+1;
}
}
void build(int x,int a,int b){
sa[x]=sb[x]=la[x]=lb[x]=0;
if(a==b){pos[a]=x;return;}
int mid=(a+b)>>1;
build(x<<1,a,mid),build(x<<1|1,mid+1,b);
}
inline void modify(int A,int B){
int x=pos[lower(B,A+B)];
alla+=A;
sa[x]+=A;
sb[x]+=B;
la[x]+=A;
lb[x]+=B;
for(x>>=1;x;x>>=1){
sa[x]+=A;
sb[x]+=B;
la[x]=la[x<<1];
lb[x]=lb[x<<1];
}
}
inline void query(){
int x=1,a=1,b=m,mid;
ll prea=0,preb=0;
Num f,g;
while(a<b){
mid=(a+b)>>1;
x<<=1;
f=Num((alla-(prea+sa[x])-(preb+sb[x]))*q[mid].u+(preb+sb[x])*q[mid].d,q[mid].d);
g=Num((alla-(prea+sa[x]+la[x+1])-(preb+sb[x]+lb[x+1]))*q[mid+1].u+(preb+sb[x]+lb[x+1])*q[mid+1].d,q[mid+1].d);
if(cmp(f,g)<0){
prea+=sa[x];
preb+=sb[x];
x++;
a=mid+1;
}else b=mid;
}
prea+=sa[x];
preb+=sb[x];
f=Num((alla-prea-preb)*q[a].u+preb*q[a].d,q[a].d);
f.write();
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d",&n);
q[1]=Num(0,1);
q[m=2]=Num(1,1);
for(i=1;i<=n;i++){
scanf("%d%d",&x,&y);
e[i].x=x;
e[i].y=y;
q[++m]=Num(y,x+y);
}
sort(q+1,q+m+1,cmpn);
for(_=0,i=1;i<=m;i++)if(i==1||cmp(q[i],q[_]))q[++_]=q[i];
m=_;
build(1,1,m);
alla=0;
for(i=1;i<=n;i++)modify(e[i].x,e[i].y),query();
}
}
E. Snowy Smile
首先將縱坐標離散化到$O(n)$的范圍內,方便后續的處理。
將所有點按照橫坐標排序,枚舉矩形的上邊界,然后往后依次加入每個點,這樣就確定了矩形的上下邊界。設$v[y]$表示矩形內部縱坐標為$y$的點的權值和,則答案為$v$的最大子段和,用線段樹維護帶修改的最大子段和即可。
時間復雜度$O(n^2\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=2010,M=4100;
int Case,n,m,i,j,k,cb,b[N],pos[N];ll pre[M],suf[M],s[M],v[M],ans;
struct E{int x,y,z;}e[N];
inline bool cmp(const E&a,const E&b){return a.x<b.x;}
void build(int x,int a,int b){
pre[x]=suf[x]=s[x]=v[x]=0;
if(a==b){
pos[a]=x;
return;
}
int mid=(a+b)>>1;
build(x<<1,a,mid),build(x<<1|1,mid+1,b);
}
inline void change(int x,int p){
x=pos[x];
s[x]+=p;
if(s[x]>0)pre[x]=suf[x]=v[x]=s[x];else pre[x]=suf[x]=v[x]=0;
for(x>>=1;x;x>>=1){
pre[x]=max(pre[x<<1],s[x<<1]+pre[x<<1|1]);
suf[x]=max(suf[x<<1|1],s[x<<1|1]+suf[x<<1]);
s[x]=s[x<<1]+s[x<<1|1];
v[x]=max(max(v[x<<1],v[x<<1|1]),suf[x<<1]+pre[x<<1|1]);
}
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d",&n);
for(cb=0,i=1;i<=n;i++){
scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].z);
b[++cb]=e[i].y;
}
sort(b+1,b+cb+1);
for(m=0,i=1;i<=cb;i++)if(i==1||b[i]!=b[m])b[++m]=b[i];
sort(e+1,e+n+1,cmp);
ans=0;
for(i=1;i<=n;i++)e[i].y=lower_bound(b+1,b+m+1,e[i].y)-b;
for(i=1;i<=n;i++)if(i==1||e[i].x!=e[i-1].x){
build(1,1,m);
for(j=i;j<=n;j=k){
for(k=j;k<=n&&e[j].x==e[k].x;k++)change(e[k].y,e[k].z);
if(ans<v[1])ans=v[1];
}
}
printf("%lld\n",ans);
}
}
F. Faraway
將$|x_i-x_e|+|y_i-y_e|$的絕對值拆掉,則每個點$(x_i,y_i)$會將平面分割成$4$個部分,每個部分里距離的表達式沒有絕對值符號,一共$O(n^2)$個這樣的區域。
枚舉每個區域,計算該區域中可能的終點數量。注意到$lcm(2,3,4,5)=60$,所以只需要枚舉$x_e$和$y_e$模$60$的余數,$O(n)$判斷是否可行,然后$O(1)$計算該區域中有多少這樣的點即可。
時間復雜度為$O(60^2n^3)$。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=15,K=60;
int Case,n,m,x,y,i,j,ca,cb,a[N],b[N];long long ans;
struct E{int x,y,k,t;}e[N];
inline int abs(int x){return x>0?x:-x;}
inline bool check(int x,int y){
for(int i=0;i<n;i++)if((abs(x-e[i].x)+abs(y-e[i].y))%e[i].k!=e[i].t)return 0;
return 1;
}
inline int cal(int l,int r){
r-=l+1;
if(r<0)return 0;
return r/K+1;
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d%d",&n,&m);
a[ca=1]=b[cb=1]=m+1;
for(i=0;i<n;i++){
scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].k,&e[i].t);
a[++ca]=e[i].x;
b[++cb]=e[i].y;
}
sort(a+1,a+ca+1);
sort(b+1,b+cb+1);
ans=0;
for(i=0;i<ca;i++)if(a[i]<a[i+1])for(j=0;j<cb;j++)if(b[j]<b[j+1])
for(x=0;x<K;x++)for(y=0;y<K;y++)if(check(a[i]+x,b[j]+y))
ans+=1LL*cal(a[i]+x,a[i+1])*cal(b[j]+y,b[j+1]);
printf("%lld\n",ans);
}
}
G. Support or Not
首先考慮找到第$k$小的球對距離,二分答案$mid$,統計有多少對球的距離不超過$mid$。我們需要找到最小的$mid$,使得有至少$k$對球的距離不超過$mid$。
將每個球的半徑都加上$\frac{mid}{2}$,那么我們的目標是統計有多少對球存在公共點。
假設最大的球半徑為$R$,以$2R$為棱長將三維空間划分為一個個立方體格子,那么每個球只需要檢查球心在附近$27$個格子內部的所有球。考慮所有球的半徑相等的情況,那么每個格子內部一旦有超過$O(\sqrt{k})$個球時,我們必然已經找到了$k$對相交的球。因此在找到$k$對相交的球時及時結束二分答案的檢查過程即可。
但是當球的半徑不盡相同時,上述分析不成立。那么在當前球的半徑不足$\frac{R}{2}$時重構網格,則最多會重構$O(\log r)$次,且每個球依然只會檢查均攤$O(\sqrt{k})$個球與它是否相交。
找到第$k$小解$ans$后,我們只需要取$mid=k-1$,繼續運行檢查算法,將找到的這些相交球對之間的距離作為最終的答案的即可,如果不足$k$個,那么剩下的答案肯定都是$ans$。
利用Hash表定位格子,則總時間復雜度為$O(n\log^2r+n\sqrt{k}\log r)$,常數較小。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=100010,M=310,inf=3000000,MO=(1<<19)-1;
unsigned int wx[inf],wy[inf],wz[inf];
int Case,n,m,lim,K,i,ans[M];
struct E{int x,y,z,r;}e[N];
inline bool cmp(const E&a,const E&b){return a.r>b.r;}
inline ll sqr(ll x){return x*x;}
inline bool check(const E&a,const E&b){return sqr(a.x-b.x)+sqr(a.y-b.y)+sqr(a.z-b.z)<=sqr(a.r+b.r+lim*2);}
inline int dis(const E&a,const E&b){
ll tmp=sqr(a.x-b.x)+sqr(a.y-b.y)+sqr(a.z-b.z);
int l=0,r=inf,mid,ret;
while(l<=r){
mid=(l+r)>>1;
if(tmp<=sqr(a.r+b.r+mid*2))r=(ret=mid)-1;else l=mid+1;
}
return ret;
}
struct EV{ull v;int w;EV*nxt;}*g[MO+7],pool[N],*cur,*p;
int pos[N],at[N],cnt,d[N],en[N],id[N],last[MO+7],CUR;
inline int ins(int A,int B,int C){
int u=(wx[A]^wy[B]^wy[C])&MO;
ull v=(((ull)A)<<42)|(((ull)B)<<21)|C;
if(last[u]<CUR)last[u]=CUR,g[u]=NULL;
for(p=g[u];p;p=p->nxt)if(p->v==v)return p->w;
cnt++;
d[cnt]=0;
p=cur++;
p->v=v;
p->w=cnt;
p->nxt=g[u];
g[u]=p;
return cnt;
}
inline int ask(int A,int B,int C){
int u=(wx[A]^wy[B]^wy[C])&MO;
ull v=(((ull)A)<<42)|(((ull)B)<<21)|C;
if(last[u]<CUR)return 0;
for(p=g[u];p;p=p->nxt)if(p->v==v)return p->w;
return 0;
}
inline void build(int st,int pre){
cnt=0;
cur=pool;
CUR++;
for(int i=st;i<=n;i++){
pos[i]=ins(e[i].x/pre,e[i].y/pre,e[i].z/pre);
d[pos[i]]++;
}
for(int i=1;i<=cnt;i++)d[i]+=d[i-1];
for(int i=1;i<=cnt;i++)en[i]=d[i];
for(int i=st;i<=n;i++)id[d[pos[i]]--]=i;
}
inline int cal(int _lim,int mode=0){
int pre=~0U>>1;
lim=_lim;
m=0;
for(int i=1;i<=n;i++){
int now=(e[i].r+lim)*2;
if(now*2<pre&&i<n)build(i+1,pre=now);
int A=e[i].x/pre,B=e[i].y/pre,C=e[i].z/pre;
for(int x=A-1;x<=A+1;x++)if(x>=0)
for(int y=B-1;y<=B+1;y++)if(y>=0)
for(int z=C-1;z<=C+1;z++)if(z>=0){
int o=ask(x,y,z);
if(!o)continue;
for(int j=en[o-1]+1;j<=en[o];j++){
int k=id[j];
if(k<=i)break;
if(check(e[i],e[k])){
m++;
if(mode)ans[m]=dis(e[i],e[k]);
if(m>=K)return m;
}
}
}
}
return m;
}
int main(){
for(wx[0]=324673,i=1;i<inf;i++)wx[i]=wx[i-1]*233+17;
for(wy[0]=812376,i=1;i<inf;i++)wy[i]=wy[i-1]*13331+97;
for(wz[0]=921375,i=1;i<inf;i++)wz[i]=wz[i-1]*10007+53;
scanf("%d",&Case);
while(Case--){
scanf("%d%d",&n,&K);
for(i=1;i<=n;i++){
scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].z,&e[i].r);
e[i].x<<=1;
e[i].y<<=1;
e[i].z<<=1;
e[i].r<<=1;
}
sort(e+1,e+n+1,cmp);
int l=0,r=inf,mid,fin;
while(l<=r){
mid=(l+r)>>1;
if(cal(mid)<K)l=mid+1;else r=(fin=mid)-1;
}
for(i=1;i<=K;i++)ans[i]=fin;
if(fin)cal(fin-1,1);
sort(ans+1,ans+K+1);
for(i=1;i<=K;i++)printf("%d\n",ans[i]);
}
}
H. TDL
考慮枚舉$f(n,m)-n$的值$t$,則$n=t\oplus k$,$O(t\log n)$檢查這個$n$是否滿足條件即可。
注意到$t$顯然不會超過第$m$個與$n$互質的質數,而$n$最多只有$O(\log\log n)<m=100$個質數,根據質數密度可以得到$t$的一個比較松的上界$O(m\log m)$。
時間復雜度$O(m^2\log^2m\log n)$。
#include<cstdio>
typedef long long ll;
int Case,m,d;ll k,ans;
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
inline ll cal(ll n,int m){
if(n<1)return 0;
for(ll i=n+1;;i++)if(gcd(n,i)==1){
m--;
if(!m)return i-n;
}
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%lld%d",&k,&m);
ans=-1;
for(d=1;d<700;d++)if(cal(k^d,m)==d){
if(ans==-1)ans=k^d;
else if(ans>(k^d))ans=k^d;
}
printf("%lld\n",ans);
}
}
I. Three Investigators
考慮將數字$a[i]$拆成$a[i]$個$a[i]$,比如4,1,2$\rightarrow$4,4,4,4,1,2,2,則問題轉化為:找到最多$5$個不共享元素的不下降子序列,使得這些子序列包含的元素總量最多。可以證明,這等於楊氏圖表前$5$層的長度之和。
考慮楊氏圖表求解答案的過程:
- 從$1$到$n$依次考慮序列中的每個數,將其插入楊氏圖表的第一層中。
- 插入$x$時,如果$x$不小於這一層的最大的數,則將$x$放在這一層的末尾;否則找到大於$x$的最小的數$y$,將$y$替換為$x$,並將$y$插入下一層。
因為每一層的元素都有序,所以可以用數組維護,尋找$y$的過程可以用二分查找加速。
但是對於本題來說,我們不能暴力地插入$a[i]$個$a[i]$。考慮將楊表每一層中相同的元素合並,用std::map記錄每個元素的個數,那么當我們一次性插入$x$個$x$時,只需要將其插入std::map中,然后不斷消費后繼,將后繼的元素個數減少即可,在減少的時候要將其作為“$p$個$q$”插入下一層中。
每一類數字被消費完畢后需要及時從std::map中刪除,而每次插入會導致最多一種其它數字被拆分,所以每層的插入次數至多為上一層的兩倍。
假設要求不超過$k$個子序列的答案,本題中$k=5$,則時間復雜度為$O(2^kn\log n)$。
#include<cstdio>
#include<map>
#include<algorithm>
using namespace std;
typedef long long ll;
const int K=5;
int Case,n,i,x;ll ans;map<int,ll>T[K];
void ins(int o,int x,ll p){
if(o>=K)return;
T[o][x]+=p;
ans+=p;
while(p){
map<int,ll>::iterator it=T[o].lower_bound(x+1);
if(it==T[o].end())return;
ll t=min(p,it->second);
ans-=t;
p-=t;
ins(o+1,it->first,t);
if(t==it->second)T[o].erase(it);else it->second-=t;
}
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d",&n);
ans=0;
for(i=0;i<K;i++)T[i].clear();
for(i=1;i<=n;i++){
scanf("%d",&x);
ins(0,x,x);
printf("%lld%c",ans,i<n?' ':'\n');
}
}
}
J. Ridiculous Netizens
取一個根,將這棵樹轉化為有根樹,考慮連通塊包含根節點的情況,那么對於一個點來說,如果它選了,它的父親就必須選。
求出DFS序括號序列,設$f[i][\lfloor\frac{m}{j}\rfloor]$表示考慮了DFS序的前$i$項,目前連通塊點權乘積為$j$的方案數。因為當$j\geq\sqrt{m}$時$\lfloor\frac{m}{j}\rfloor$只有$O(\sqrt{m})$種取值,所以狀態數為$O(n\sqrt{m})$。注意到$\lfloor\frac{m}{jk}\rfloor=\lfloor\frac{\lfloor\frac{m}{j}\rfloor}{k}\rfloor$,所以可以轉移。
如果$i$是一個左括號,那么把$f$傳給兒子,並強制選擇兒子;如果$i$是個右括號,那么這個子樹既可以選又可以不選,將對應狀態的方案數累加即可,轉移$O(1)$。
接下來考慮連通塊不包含根節點的情況,那么可以去掉這個根,變成若干棵樹的子問題。取重心作為根進行點分治,則考慮的總點數為$O(n\log n)$。
時間復雜度$O(n\sqrt{m}\log n)$。
#include<cstdio>
const int N=2010,K=2010,P=1000000007;
int Case,n,m,cnt,val[K],i,x,y,a[N],ans;
int g[N],nxt[N<<1],v[N<<1],ok[N<<1],ed,son[N],f[N],all,now;
int dp[N][K],tmp[K];
inline void up(int&a,int b){a=a+b<P?a+b:a+b-P;}
inline void add(int x,int y){v[++ed]=y;nxt[ed]=g[x];ok[ed]=1;g[x]=ed;}
void findroot(int x,int y){
son[x]=1;f[x]=0;
for(int i=g[x];i;i=nxt[i])if(ok[i]&&v[i]!=y){
findroot(v[i],x);
son[x]+=son[v[i]];
if(son[v[i]]>f[x])f[x]=son[v[i]];
}
if(all-son[x]>f[x])f[x]=all-son[x];
if(f[x]<f[now])now=x;
}
void dfs(int x,int y){
int i,j,k=a[x];
for(i=1;i<=cnt;i++)tmp[i]=0;
for(i=j=1;i<=cnt;i++){
int t=val[i]/k;
if(!t)continue;
while(val[j]>t)j++;
up(tmp[j],dp[x][i]);
}
for(i=1;i<=cnt;i++)dp[x][i]=tmp[i];
for(i=g[x];i;i=nxt[i])if(ok[i]){
int u=v[i];
if(u==y)continue;
for(j=1;j<=cnt;j++)dp[u][j]=dp[x][j];
dfs(u,x);
for(j=1;j<=cnt;j++)up(dp[x][j],dp[u][j]);
}
}
void solve(int x){
int i;
for(i=1;i<=cnt;i++)dp[x][i]=0;
dp[x][1]=1;
dfs(x,0);
for(i=1;i<=cnt;i++)up(ans,dp[x][i]);
for(i=g[x];i;i=nxt[i])if(ok[i]){
ok[i^1]=0;
f[0]=all=son[v[i]];
findroot(v[i],now=0);
solve(now);
}
}
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d%d",&n,&m);
cnt=ans=0;
for(i=1;i<=n;i++)g[i]=son[i]=f[i]=0;
for(i=1;i<=m;i=m/(m/i)+1)val[++cnt]=m/i;
for(i=1;i<=n;i++)scanf("%d",&a[i]);
for(ed=i=1;i<n;i++)scanf("%d%d",&x,&y),add(x,y),add(y,x);
f[0]=all=n;findroot(1,now=0);solve(now);
printf("%d\n",ans);
}
}
K. 11 Dimensions
設$f[i][j]$表示$[1,i-1]$這些位的數字已經確定,且$[1,i-1]$的數字模$m=j$時,有多少種在$[i,n]$這些位填數字的方法使得最終的數字模$m=0$。
初始值:$f[n+1][0]=1$。
轉移:$f[i][j]=\sum f[i+1][(10j+k)\bmod m]$,其中$0\leq k\leq 9$,且第$i+1$位可以填$k$。
最終滿足條件的總方案數即為$f[1][0]$,這樣就可以判斷每個詢問是否有解。
考慮在有解的情況下如何找到第$k$小的方案。我們稱如果狀態$i$由狀態$j$等累加得到,則$j$是$i$的一個后繼狀態。從初始狀態$(1,0)$開始,按照下一位填的數字從小到大枚舉當前狀態$S$的每個后繼狀態$T$,如果$T$的DP值$\geq k$,則說明我們要找的方案在$T$中,且這個方案下這一位已經確定,然后走到$T$狀態即可;否則$T$的DP值$<k$,那么將$k$減去$T$的DP值,然后繼續考慮其它更大的后繼即可。這樣單次詢問是$O(n)$的,不能接受。
類似樹的輕重鏈剖分,對於每個狀態,取其后繼狀態中DP值最大的狀態作為重后繼,則每個狀態最多只有一個重后繼,我們可以倍增求出每個狀態往后走$2^k$次重后繼后會到達哪個狀態,以及那個狀態相對當前來說是第幾小的方案。對於每個詢問,我們先在倍增數組中沿着重后繼不斷往前走直到必須要走輕后繼為止,然后走一次輕后繼,再接着沿着倍增數組走重后繼。
因為重后繼是DP值最大的后繼,這意味着每個輕后繼的DP值不超過總方案數的一半,所以每走一次輕后繼,$k$至少會除以二,最多$O(\log k)$次。
時間復雜度$O(nm\log n+q\log n\log k)$。
#include<cstdio>
typedef long long ll;
const ll inf=1000000000000000010LL;
const int N=50010,M=20,K=17,P=1000000007;
int Case,n,m,q,i,j,k,p[N];ll _;
char a[N];
int g[M][10];
bool can[N][10];
ll f[N][M],st[K][N][M],en[K][N][M];
char go[K][N][M];
int val[K][N][M];
inline ll fix(ll x){return x<inf?x:inf;}
inline int query(ll k){
if(k>f[1][0])return -1;
int x=1,y=0,ret=0,i;
while(x<=n){
for(i=K-1;~i;i--)if(x+(1<<i)<=n+1&&st[i][x][y]<k&&k<=en[i][x][y]){
ret=(1LL*ret*p[1<<i]+val[i][x][y])%P;
k-=st[i][x][y];
y=go[i][x][y];
x+=1<<i;
}
if(x>n)break;
for(i=0;i<10;i++)if(can[x][i]){
ll tmp=f[x+1][g[y][i]];
if(k>tmp)k-=tmp;
else{
ret=(10LL*ret+i)%P;
x++;
y=g[y][i];
break;
}
}
}
return ret;
}
int main(){
for(p[0]=i=1;i<N;i++)p[i]=10LL*p[i-1]%P;
scanf("%d",&Case);
while(Case--){
scanf("%d%d%d%s",&n,&m,&q,a+1);
for(i=0;i<m;i++)for(j=0;j<10;j++)g[i][j]=(i*10+j)%m;
for(i=1;i<=n;i++){
if(a[i]=='?')for(j=0;j<10;j++)can[i][j]=1;
else{
for(j=0;j<10;j++)can[i][j]=0;
can[i][a[i]-'0']=1;
}
}
for(j=0;j<m;j++)f[n+1][j]=j==0;
for(i=n;i;i--)for(j=0;j<m;j++){
ll tmp=0;
int nxt=-1;
ll sz=-1;
for(k=0;k<10;k++)if(can[i][k]){
ll now=f[i+1][g[j][k]];
tmp=fix(tmp+now);
if(now>sz)nxt=k,sz=now;
}
f[i][j]=tmp;
go[0][i][j]=g[j][nxt];
val[0][i][j]=nxt;
ll sum=0;
for(k=0;k<nxt;k++)if(can[i][k])sum=fix(sum+f[i+1][g[j][k]]);
st[0][i][j]=sum;
en[0][i][j]=fix(sum+f[i+1][g[j][nxt]]);
}
for(k=1;k<K;k++)for(i=1;i+(1<<k)<=n+1;i++)for(j=0;j<m;j++){
int x=go[k-1][i][j],len=1<<(k-1);
go[k][i][j]=go[k-1][i+len][x];
val[k][i][j]=(1LL*val[k-1][i][j]*p[len]+val[k-1][i+len][x])%P;
st[k][i][j]=fix(st[k-1][i][j]+st[k-1][i+len][x]);
en[k][i][j]=fix(st[k-1][i][j]+en[k-1][i+len][x]);
}
while(q--)scanf("%lld",&_),printf("%d\n",query(_));
}
}
L. Stay Real
小根堆中,每個點的權值總是不小於父親節點的權值。所以無論怎么取,先拿走的數一定不小於后面拿走的數。
此時雙方的最優策略就是:貪心選擇能取的數字之中最大的數。
時間復雜度$O(n\log n)$。
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=100010;
int Case,n,i,j,a[N];ll A,B;
int main(){
scanf("%d",&Case);
while(Case--){
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&a[i]);
sort(a+1,a+n+1);
A=B=0;
for(i=n,j=1;i;i--,j^=1)if(j)A+=a[i];else B+=a[i];
printf("%lld %lld\n",A,B);
}
}
