2019-2020 XX Open Cup, Grand Prix of Korea
2019-2020 XX Open Cup, Grand Prix of Korea
比賽收穫
本場貢獻:et3_tsy :G提供了思路,過了H,H爆了int,貢獻WA一發,J有想法,調了1hr後,發現在特例情況複雜度退化,假了
1427314831a:過了A
Ryker0923 :過了G,提供了A的思路
一共三道題
總結:本場暴露兩個問題:
1)後期乏力
前兩個小時過了三題,接下來,FIJ都可以做,但是都沒有比較好的想法,說明題量不夠,還需要時間沉澱。
2)特例導致複雜度退化
演算法在實現之前,一定要和隊友多交流,多嘗試幾種特例,一方面看演算法錯沒錯,一方面還要看複雜度會不會退化。像
部分題解
A. 模擬
簡單模擬即可,注意很多細節上的問題,以及不要重複計數
#include<bits/stdc++.h> using namespace std; int a[1000][1000],b[1000][1000],n,m; int cmp(int x,int y) { if(x==6&&y==6)return 1; if(x==6&&y==9)return 1; if(x==7&&y==7)return 1; if(x==8&&y==8)return 1; if(x==9&&y==9)return 1; if(x==9&&y==6)return 1; return 0; } int main() { cin>>n>>m; char c=getchar(); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { a[i][j]=getchar()-'0'; } getchar(); } for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { b[n-i+1][m-j+1]=a[i][j]; } for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { if(b[i][j]==6)b[i][j]=9; else if(b[i][j]==9)b[i][j]=6; } int bj=0; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { if(!cmp(a[i][j],b[i][j])) { bj=1;break; } } if(!bj) { if(n%2==1&&m%2==1&&a[n/2+1][m/2+1]!=8) bj=1; } if(bj){ cout<<-1;return 0; } int ans=0; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { if(a[i][j]!=b[i][j])ans++; else if(a[i][j]==7)ans++; } cout<<ans/2; return 0; }
G. 圖論
注意這道題是讓我們從可行解裡面選取最小字典序的路徑,那麼換言之,我們就要優先dfs去查詢哪個點是能到達目標點的。
值得注意的是,因為在求向圖可達性中,可能形成強連通,所以直接進行兩次連續的dfs,防止漏掉點沒有被標記的可達點
最後是進行對可行點形成的單向路徑上進行貪心模擬的過程。至於什麼時候輸出too long?只要訪問了已經訪問過的點,就說明它在已有的一個強連通分量裡面出不來了,故輸出too long即可。
#include<bits/stdc++.h> using namespace std; int n,m,s,t,cnt,tot; int l[200010],rd[2000010]; struct node{ int x,y,z; }ed[600010]; bool cmp(node a,node b) { return a.z>b.z; } struct tedge{ int v,next,w; bool u; }e[1000010]; bool b[200010],used[200010]; void add(int x,int y,int z) { e[++cnt].w=y; e[cnt].v=z; e[cnt].next=l[x]; l[x]=cnt; } bool dfs(int x) { if(used[x]) return b[x]; used[x]=1; for(int i=l[x];i;i=e[i].next) { // cout<<x<<" "<<e[i].w<<endl; if(b[x]==1) dfs(e[i].w); else b[x]=dfs(e[i].w); } return b[x]; } void print() { for(int i=1;i<=tot;i++) printf("%d ",rd[i]); } void dfs2(int x) { used[x]=1; for(int i=l[x];i;i=e[i].next) if(b[e[i].w]) { rd[++tot]=e[i].v; if(tot>1000000) { printf("TOO LONG"); exit(0); } if(e[i].w==t) { print(); exit(0); } else if(used[e[i].w]) { printf("TOO LONG"); exit(0); } else dfs2(e[i].w); } } int main() { cin>>n>>m>>s>>t; int x,y,z; b[t]=1; for(int i=1;i<=m;i++) { scanf("%d%d%d",&ed[i].x,&ed[i].y,&ed[i].z); } sort(ed+1,ed+m+1,cmp); for(int i=1;i<=m;i++) { // cout<<ed[i].x<<" "<<ed[i].y<<" "<<ed[i].z<<endl; add(ed[i].x,ed[i].y,ed[i].z); } dfs(s); for(int i=1;i<=n;i++)used[i]=0; // for(int i=1;i<=n;i++)cout<<b[i]<<endl; dfs(s); if(!b[s]) { printf("IMPOSSIBLE"); return 0; } // for(int i=1;i<=n;i++)cout<<b[i]<<endl; for(int i=1;i<=n;i++)used[i]=0; dfs2(s); } /* 3 3 1 3 1 2 1 2 1 2 1 3 3 */
H. 思維
給定兩個全排列,全排列A可動,B不能動,求最小交換次數,使得Ai-Bi的絕對值之和最大。
應該先從偶數上思考,即什麼時候最優,即把 1到 N/2 分為第一組,把 N/2+1 到 N分為第二組
那麼我們只要保證對於任意一對Ai和Bi恰好一個屬於第一組,一個屬於第二組即可,顯然這樣最優。
而奇數大同小異,唯有不同的是最中間的那個數,它不能分到第一組和第二組中去。
接下來就是雙指標貪心掃描了,分別第二組元素為第一組、第二組的情況,取min即可,注意會爆int
#include<bits/stdc++.h>
using namespace std;
#define maxn 300000
#define ll long long
#define INF 0x3f3f3f3f3f3f3f3fLL
int a[maxn],b[maxn];
ll minn(ll x,ll y)
{
return x<y?x:y;
}
int n;
double mid;
bool l(int val)
{
return val<mid;
}
bool r(int val)
{
return val>mid;
}
int main()
{
cin>>n;
mid=(0.0+n)/2+0.5;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
ll ans=INF;
ll cur=0;
int cnt=0;
int apos=0,bpos=0;
cur=0;
while(cnt!=n/2)
{
while(!l(b[++bpos]));
while(!r(a[++apos]));
cur+=abs(apos-bpos);
cnt++;
}
ans=minn(ans,cur);
cur=0;cnt=0;apos=0,bpos=0;
while(cnt!=n/2)
{
while(!r(b[++bpos]));
while(!l(a[++apos]));
cur+=abs(apos-bpos);
cnt++;
}
ans=minn(ans,cur);
cout<<ans<<"\n";
return 0;
}
J.啟發式合併
大致題意(經過轉化之後的)
給定n個區段【l, r】, 以及對應的權值,注意他們不交叉
現在就是求 k (從1->n分別求) 層,至多疊k層的最大權值和為多少
思路
顯然,我們可以發現他們不交叉只有兩種情況,一種是完全包含,一種是完全不相交
這就有一個樹的特徵了,建樹先排個序,左端第一維(越左越優),區段長第二維(越長越優),再線性掃一遍。
我們把樹建好後,很顯然可以在樹上去算dp,但是會被下圖的特例卡退化成\(O(n^2)\)
那麼我們應該怎麼想這道題嘞?
其實有個很核心的性質
如果我們在第k層進行貪心選擇的時候,k+1層也一定會選上這些
這一點一定要理解好
那麼我們理解了這個之後,只要利用堆,去儲存單層最優的區段貢獻即可(對於兄弟之間而言,他們不會產生影響,所以取出兩邊最優的相加,就是父親的最優)
而這一過程可以利用啟發式合併的思想進行優化處理
對於本題, et3_tsy 有一定的收穫:
對於啟發式合併而言,並不一定要依賴於樹剖,它只是一種思想,就是把小的往大的上面去合併。在進行樹dp的時候,一定優先考慮退化問題(比如這題可能會退化成如下情況)。
#include<bits/stdc++.h>
using namespace std;
#define maxn 252000
#define ll long long
int n,tot;
ll tmp[maxn];
int ma[maxn],in[maxn],fa[maxn];
priority_queue<ll>heap[maxn];
inline int read()
{
int ans=0;
char last=' ',ch=getchar();
while(ch<'0'|ch>'9')last=ch,ch=getchar();
while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
if(last=='-')ans=-ans;
return ans;
}
void merge_(int x)
{
int f=fa[x],fh=ma[f],gh=ma[x];
if(heap[fh].size()<heap[gh].size())
{
ma[f]=gh;
swap(fh,gh);
}
int cnt=0;
while(!heap[gh].empty())
{
tmp[cnt++]=heap[gh].top()+heap[fh].top();
heap[gh].pop(),heap[fh].pop();
}
for(int i=0;i<cnt;i++)heap[fh].push(tmp[i]);
}
struct ed
{
int l,r;
ll val;
bool operator<(const ed& sec)const
{
if(l!=sec.l)return l<sec.l;
return r>sec.r;
}
}e[maxn];
inline bool check(int l,int r,int curl,int curr)
{
return l<=curl&&r>=curr;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
e[i].l=read(),e[i].r=read(),e[i].val=read();
e[i].r--;
}
sort(e+1,e+n+1);
int curindex=0;
for(int i=1;i<=n;i++)
{
while(curindex&&!check(e[curindex].l,e[curindex].r,e[i].l,e[i].r))
{
curindex=fa[curindex];
}
fa[i]=curindex;
curindex=i;
}
queue<int>node;
for(int i=1;i<=n;i++)
{
ma[i]=i;
in[fa[i]]++;
}
for(int i=1;i<=n;i++)
if(in[i]==0)node.push(i);
while(!node.empty())
{
int x=node.front();
node.pop();
if(x==0)break;
heap[ma[x]].push(e[x].val);
merge_(x);
in[fa[x]]--;
if(in[fa[x]]==0)node.push(fa[x]);
}
ll ans=0;
for(int i=1;i<=n;i++)
{
if(!heap[ma[0]].empty())
{
ans+=heap[ma[0]].top();
heap[ma[0]].pop();
}
cout<<ans<<" ";
}
return 0;
}
F. Hilbert's Hotel 線段樹
題意:有一家旅館(有多個房間,序號從0開始)和多個旅行團,現給定三種操作:
1 k
表示新來一個旅行團有k個人.
當k>0時所有旅館中已有的人往後移k個房間;
當k=0時表示來了數不清的人,此時所有旅館中已有的人移到自己房間標號2倍的房間,再將新加入的人安排在房間號為奇數的房間中。
2 g x
訪問第g個旅行團的第x小的人在那個房間,結果對le9+7取模(g<=當前已有的旅行團個數,1<=x<=第g個旅行團的人數)
3 x
訪問當前第x個房間的人來自那個旅行團(x<=1e9)
題解:
對於第二種詢問,我們可以考慮對每一個旅行團的人所在位置用一個函式\(y=kx+b\)維護
當k>0時將所有的已有的bi加上k,再新加入一組\(k=1,b=1e9+6\)
當k=0時將所有已有的ki和bi乘2,再新加入一組\(k=2,b=1e9+6\)
開兩顆線段樹分別去維護k和b即可。
對於第三種詢問,我們考率從x這個位置向前回溯已有的操作,以確定x來自那個旅行團
如若當前k>0時,如果k>x則x一定來自當前操作所在的旅行團,否則令x-=k,回溯上一步操作
若當前k=0,如果x為奇數,則x來自當前旅行團,否則令x=x/2,回溯上一步操作
對於k>0的回溯訪問,暴力維護複雜度為n,考慮維護所有連續的k>0的和,在此區間上進行二分答案,即可優化為logn
對於k=0的回溯訪問,當x>0時每次至少減少一半,複雜度為logn,但若一開始時x=0,則需跳過這個連續的k=0的區間,否則會進行多次無意義的除二的操作。
#include<iostream>
#include<algorithm>
using namespace std;
const int p=1e9+7;
struct tnode{
long long l,r,num,plz,mlz;
}t[1200040][2];
long long x,y,z,bj,a[300010],binary[300010],caozuo[300100],l1[300010],l2[300010],b[300010];
inline void build(int g,int i,int l,int r)
{
t[i][g].l=l;t[i][g].r=r;
if(l==r){
// t[i][g].num=a[l]%p;
return;
}
int mid=(l+r)>>1;
build(g,i<<1,l,mid);
build(g,(i<<1)+1,mid+1,r);
t[i][g].num=(t[i<<1][g].num+t[(i<<1)+1][g].num)%p;
}
inline void pushdown(int g,int i)
{
long long k1=t[i][g].plz,k2=t[i][g].mlz;
t[i<<1][g].num=(t[i<<1][g].num*k2+k1*(t[i<<1][g].r-t[i<<1][g].l+1))%p;
t[(i<<1)+1][g].num=(t[(i<<1)+1][g].num*k2+k1*(t[(i<<1)+1][g].r-t[(i<<1)+1][g].l+1))%p;
t[i<<1][g].plz=(t[i<<1][g].plz*k2+k1)%p;
t[(i<<1)+1][g].plz=(t[(i<<1)+1][g].plz*k2+k1)%p;
t[i<<1][g].mlz=t[i<<1][g].mlz*k2%p;
t[(i<<1)+1][g].mlz=t[(i<<1)+1][g].mlz*k2%p;
t[i][g].plz=0;
t[i][g].mlz=1;
return;
}
inline void add(int g,int i,int l,int r,long long k)
{
if(t[i][g].l>r||t[i][g].r<l)return;
if(l<=t[i][g].l&&r>=t[i][g].r)
{
t[i][g].num=(t[i][g].num+k*(t[i][g].r-t[i][g].l+1))%p;
t[i][g].plz=(t[i][g].plz+k)%p;
return;
}
pushdown(g,i);
if(t[i<<1][g].r>=l)add(g,i<<1,l,r,k);
if(t[(i<<1)+1][g].l<=r)add(g,(i<<1)+1,l,r,k);
t[i][g].num=(t[i<<1][g].num+t[(i<<1)+1][g].num)%p;
return;
}
inline void mul(int g,int i,int l,int r,long long k)
{
if(t[i][g].l>r||t[i][g].r<l)return;
if(l<=t[i][g].l&&t[i][g].r<=r)
{
t[i][g].num=t[i][g].num*k%p;
t[i][g].plz=t[i][g].plz*k%p;
t[i][g].mlz=t[i][g].mlz*k%p;
return;
}
pushdown(g,i);
if(t[i<<1][g].r>=l)mul(g,i<<1,l,r,k);
if(t[(i<<1)+1][g].l<=r)mul(g,(i<<1)+1,l,r,k);
t[i][g].num=(t[i<<1][g].num+t[(i<<1)+1][g].num)%p;
return;
}
inline long long sum(int g,int i,int l,int r)
{
if(t[i][g].l>r||t[i][g].r<l)return 0;
if(t[i][g].l>=l&&t[i][g].r<=r)
{
return t[i][g].num%p;
}
pushdown(g,i);
long long res=0;
if(t[i<<1][g].r>=l)res=(res+sum(g,i<<1,l,r))%p;
if(t[(i<<1)+1][g].l<=r)res=(res+sum(g,(i<<1)+1,l,r))%p;
return res%p;
}
int main()
{
//freopen("std.in","r",stdin);
//freopen("std.out","w",stdout);
int m;
cin>>m;
build(0,1,0,m);
build(1,1,0,m);
for(int i=1;i<=4*m;i++)
{
t[i][0].mlz=1;
t[i][1].mlz=1;
}
// cout<<sum(1,2,n)<<endl;
int k,tot=0;
add(0,1,0,0,1);
add(1,1,0,0,p-1);
while(m--)
{
scanf("%d",&bj);
if(bj==1)
{
tot++;
scanf("%d",&k);
if(k)
{
a[tot]=k;
if(caozuo[tot-1]==1)l1[tot]=l1[tot-1];
else l1[tot]=tot;
caozuo[tot]=1;
add(1,1,0,tot-1,k);
add(1,1,tot,tot,p-1);
add(0,1,tot,tot,1);
}
else
{
caozuo[tot]=2;
if(caozuo[tot-1]==2)l2[tot]=l2[tot-1];
else l2[tot]=tot;
mul(0,1,0,tot-1,2);
mul(1,1,0,tot-1,2);
add(0,1,tot,tot,2);
add(1,1,tot,tot,p-1);
}
a[tot]+=a[tot-1];
}
if(bj==2)
{
int g,x;
scanf("%d%d",&g,&x);
k=sum(0,1,g,g);
int b=sum(1,1,g,g);
printf("%lld\n",(1ll*k*x+b)%p);
}
if(bj==3)
{
int x;
scanf("%d",&x);
int cnt=tot;
while(cnt>0)
{
if(caozuo[cnt]==1)
{
int l=l1[cnt],r=cnt;
if(a[r]-a[l-1]<=x)
{
x-=a[r]-a[l-1];
cnt=l-1;
}
else
{
int mid=(l+r)>>1;
while(l<r-1)
{
mid=(l+r)>>1;
if(a[cnt]-a[mid-1]>x)l=mid;
else r=mid;
}
if(x<a[cnt]-a[r-1])
{
cnt=r;break;
}
else
{
cnt=l;break;
}
}
}
else
{
if(x&1)break;
if(x==0)
{
cnt=max(l2[cnt]-1,0ll);
break;
}
else
{
x>>=1;
cnt--;
}
}
}
printf("%d\n",cnt);
}
}
return 0;
}
I.最小直徑生成樹
這道是一道板子題
求最小直徑生成樹,在oi-wiki上有相關內容
先用Floyd或者Johnson跑出全員最短路
再對每個點排序一遍去確定自遠及近的點是誰
下面來確定絕對中心
絕對中心可能是邊,可能是點
對於點來講,它只要選擇最遠的兩個點作為最遠的點,其他點都比他小,相加就是他們的直徑。跑一遍迪傑斯特拉即可
對於邊來說,我們找到絕對中心對應的邊之後,對於它兩個邊上的點u,v初始化不應該都是0,否則就會出現下面的錯誤,。假定我們的絕對中心在4這條藍色邊上,它兩端初始化的dis均是0,對於箭頭指向的點,選綠3,或者紫1,在dij中均是等價的,但是我們很容易發現,在直徑的求解中,如果選紫1顯然它的值會更小(因為它的產生的代價已經由黑3承擔了)
為了處理這個問題,最小直徑生成樹提出了一個思路
它的絕對中心一開始在中間,誰的最遠距離遠就離誰近
如果記絕對中心距離u點的距離為\(disu\),由u點支配的最遠的點距離u的距離為\(faru\)
絕對中心距離v點的距離為\(disv\),由v點支配的最遠的點距離v的距離為\(farv\)
絕對中心所屬的這條邊權值是\(w.val\),那麼有:
$disu=(w.val+farv-faru){\div}2 $
$disv=(w.val+faru-farv){\div}2 $
上面的問題就引刃而解了,即絕對中心會更加靠近最遠的點
很顯然,這一類題目很容易爆int以及用精度寫有點浪費,所以乾脆全部乘以2,以及後面的dij壓邊的時候記得全部乘以2即可
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define maxn 505
#define INF 0x3f3f3f3f3f3f3f3fLL
#define int ll
inline ll read()
{
ll ans=0;
char last=' ',ch=getchar();
while(ch<'0'|ch>'9')last=ch,ch=getchar();
while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
if(last=='-')ans=-ans;
return ans;
}
ll min(ll a,ll b)
{
return a>b?b:a;
}
int n,m,a,b,c;
ll initu=0;
int ansu=0,ansv=0;
ll curans=INF;
struct E
{
int u,v,val;
E(int a,int b,int c){u=a;v=b;val=c;}
};
struct so
{
int val;
int id;
bool operator<(const so& sec)const
{
return val>sec.val;
}
}ss[maxn<<2];
struct dij
{
ll val;
int nxt;
int from;
bool operator<(const dij& sec)const
{
return val>sec.val;
}
dij(ll a,int b,int c){val=a;nxt=b;from=c;}
};
vector<E>e;
vector<int>node[maxn];
ll dis[maxn][maxn];
int opt[maxn][maxn];
ll vis[maxn];
bool been[maxn];
priority_queue<dij>heap;
void getans(int curnum)
{
E& cured=e[curnum];
int u=cured.u;
int v=cured.v;
for(int p=1,i=2;i<=n;i++)
{
if(dis[v][opt[u][i]]>dis[v][opt[u][p]])
{
if(curans>cured.val+dis[u][opt[u][i]]+dis[v][opt[u][p]])
{
curans=cured.val+dis[u][opt[u][i]]+dis[v][opt[u][p]];
initu=curans-dis[u][opt[u][i]]*2;
ansu=u,ansv=v;
}
p=i;
}
}
return;
}
signed main()
{
n=read(),m=read();
memset(dis,INF,sizeof(dis));
for(int i=0;i<=n;i++)dis[i][i]=0;
for(int i=0;i<m;i++)
{
a=read(),b=read(),c=read();
node[a].push_back(e.size());
e.emplace_back(a,b,c);
node[b].push_back(e.size());
e.emplace_back(b,a,c);
dis[a][b]=c;
dis[b][a]=c;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(dis[i][k]+dis[k][j]<dis[i][j])dis[i][j]=dis[i][k]+dis[k][j];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
ss[j].id=j,ss[j].val=dis[i][j];
sort(ss+1,ss+n+1);
for(int j=1;j<=n;j++)
opt[i][j]=ss[j].id;
}
for(int i=0;i<e.size();i+=2)
{
getans(i);
}
for(int i=1;i<=n;i++)
{
if(dis[i][opt[i][1]]+dis[i][opt[i][2]]<curans)
{
curans=dis[i][opt[i][1]]+dis[i][opt[i][2]];
ansu=ansv=i;
}
}
cout<<curans<<"\n";
memset(vis,INF,sizeof(vis));
vis[ansu]=initu;
heap.push(dij(vis[ansu],ansu,ansu));
if(ansu!=ansv)
{
vis[ansv]=dis[ansv][ansu]*2-initu;
heap.push(dij(vis[ansv],ansv,ansu));
}
int curcnt=0;
while(!heap.empty()&&curcnt<n)
{
dij curx=heap.top();heap.pop();
if(been[curx.nxt])continue;
been[curx.nxt]=1;
curcnt++;
if(curx.nxt!=curx.from)cout<<curx.nxt<<" "<<curx.from<<"\n";
for(int k:node[curx.nxt])
{
int nxt=e[k].v;
if(!been[nxt]&&vis[curx.nxt]+e[k].val*2<vis[nxt])
{
vis[nxt]=vis[curx.nxt]+e[k].val*2;
heap.push(dij(vis[nxt],nxt,curx.nxt));
}
}
}
return 0;
}