noip 2017題解
一、小凱的疑惑:
去年比賽時懵逼報零的題目,考完之後大家告訴我是小學奧數,直接ab-a-b就A了。之後去向別人請教,某些人說:這題就是一道打表題,直接打表就行了,或者看洛谷的題解上面大多都是在證明為什麼ab-a-b是答案是正確的,可是我考慮的是,這個ab-a-b從何而來?題解大多數好像並沒有提到,或者有的dalao寫的exgcd我也不清楚是怎麼求的,機房的學長去年有寫exgcd的但是掛掉了。 今天,藉助某位dalao的思路,我來為大家提供一種新的簡單的思路(畢竟這是noip的第一題,不會那麼難吧?)a與b是互質的,我們可以無限使用a和b來組成一些數,我們要求的是最大的那個不能有a和b組成的數。假設只有b,那麼b可以組成的數一定是0,b,2b……那麼a和b組合起來的作用在哪兒呢?學過同餘的都知道,只使用b的時候,我們關於模b的剩餘系中就只枚舉出來了餘數為0的情況。a的作用就是把它乘上某一個倍數,使它出現模b的另一個剩餘系。我們發現,因為a,b互質,只要把a乘上b-1次,此時就已經出現了所有的剩餘系。這時候到當前這個數,我們其實已經覆蓋了模b的所有剩餘系,所以它之後的所有數都可以由a,b組成。而對於之前的數:到a(b-1)這個數的時候我們剛好覆蓋了所有的剩餘系,設a(b-1)%b=r,那麼說明在這之前模b餘數為r的值是無法組成的,而從它到a(b-1)這中間的剩餘系因為在之前已經被覆蓋了,所以都可以組成。那麼最大的不能組成的數就是a(b-1)-b,也就是a*b-a-b。
二、乳酪:
貌似思路很多?有人寫的並查集,有人寫的最短路好像,我是直接建圖然後跑dfs。noip的時候也是這麼寫的,具體怎麼掛成40了我也記不清楚了,總之是送分題。
#include<bits/stdc++.h> #define ll unsigned long long using namespace std; const int maxn=1e3+10; int n,t,h,r,tot;bool flag; int ver[maxn*maxn],Next[maxn*maxn],lin[maxn]; int x[maxn],y[maxn],z[maxn];bool v[maxn]; void add(int x,int y) { ver[++tot]=y;Next[tot]=lin[x];lin[x]=tot; } void dfs(int x) { if(x==n+1){ flag=1;return;} for(int i=lin[x];i;i=Next[i]){ int y=ver[i]; if(!v[y]){ v[y]=1; dfs(y); } } return ; } int main() { scanf("%d",&t); while(t--){ memset(Next,0,sizeof(ver)); memset(lin,0,sizeof(lin)); scanf("%d%d%d",&n,&h,&r); for(int i=1;i<=n;i++){ scanf("%d%d%d",&x[i],&y[i],&z[i]); } for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++){ if(2*r>=sqrt(1.0*(x[i]-x[j])*(x[i]-x[j])+1.0*(y[i]-y[j])*(y[i]-y[j])+1.0*(z[i]-z[j])*(z[i]-z[j]))) add(i,j),add(j,i); } for(int i=1;i<=n;i++) if(r>=z[i]) add(0,i),add(i,0); for(int i=1;i<=n;i++) if(z[i]+r>=h) add(i,n+1),add(n+1,i); memset(v,0,sizeof(v)); v[0]=1;flag=0; dfs(0); if(flag) printf("Yes\n"); else printf("No\n"); } return 0; }
三、逛公園
設1到N的最短路為d,題目讓求的就是,關於給定的圖,從1到N路徑長度不大於d+k的路徑數。然後題目中可以存在零環。如果有無數解,就輸出-1。
簡單分析一下,我們要做的有兩件事,一個是判斷是否存在零環並且計算會不會對答案做出貢獻,如果是,那麼一定有無陣列解,反之則零環對答案沒有影響。 第二件事就是求路徑數了。
30分暴力是k=0,並且沒有零環,那我們只需要做最短路計數就行。
70分是沒有零環的情況,我們考慮如何求解。其實看了一下資料,k不大,我們可以在圖上做dp。設f【x】【i】表示從1走到x,路徑長度為d【x】+i的方案數。每一個狀態只能由它相鄰的點更新,然後寫遞推式或者記憶化搜尋就可以了。
100分的寫法:其實我們可以第一件事就做零環,我們把所有的零環找出來,然後用並查集把每一個零環上的點合併起來成一個點,判斷1到這個點的最短距離加上這個點到N的最短距離,是否小於等於d+k,如果有一個點符合條件,就輸出-1.如果所有的都不符合,說明不存在無數解,就可以再開始做dp就可以了。當然其實如果你寫的dp是記憶化搜尋,不用寫並查集那麼麻煩。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=2e5+10;
int t,n,m,k,p,tot,ver[M],Next[M],lin[N],edge[M],d[N],v[N],vc[M],Nex[M],lc[N];
int f[N][60],flag[N][60];
void add(int x,int y,int z){
ver[++tot]=y;Next[tot]=lin[x];lin[x]=tot;edge[tot]=z;
vc[tot]=x;Nex[tot]=lc[y];lc[y]=tot;
}
void dijkstra(){
priority_queue<pair<int,int > >q;
memset(d,0x3f,sizeof(d));
d[1]=0;q.push(make_pair(0,1));
while(q.size()){
int x=q.top().second;q.pop();
if(v[x]) continue;
v[x]=1;
for(int i=lin[x];i;i=Next[i]){
int y=ver[i];
if(d[y]>d[x]+edge[i]){
d[y]=d[x]+edge[i];
q.push(make_pair(-d[y],y));
}
}
}
}
int dfs(int x,int l){
if(l<0||l>k) return 0;
if(flag[x][l]){
flag[x][l]=0;
return -1;
}
if(f[x][l]!=-1) return f[x][l];
int ans=0;
flag[x][l]=1;
for(int i=lc[x];i;i=Nex[i]){
int y=vc[i];
int val=dfs(y,d[x]+l-edge[i]-d[y]);
if(val==-1){
flag[x][l]=0;
return -1;
}
ans=(long long)(ans+val)%p;
}
flag[x][l]=0;
if(x==1&&l==0) ans++;
f[x][l]=ans;
return ans;
}
int solve(){
dijkstra();
int ans=0;
for(int i=0;i<=k;i++){
int val=dfs(n,i);
if(val==-1) return -1;
ans=(long long)(ans+val)%p;
}
return ans;
}
int main(){
scanf("%d",&t);
while(t--){
tot=0;
scanf("%d%d%d%d",&n,&m,&k,&p);
for(int i=1;i<=n;i++) lin[i]=lc[i]=v[i]=0;
for(int i=1;i<=m;i++) Nex[i]=Next[i]=edge[i]=0;
memset(f,-1,sizeof(f));
memset(flag,0,sizeof(flag));
for(int i=1;i<=m;i++){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
printf("%d\n",solve());
}
}
四、時間複雜度
這道題就是棧的純模擬,就是處理的時候麻煩了一些。
我們發現,在不考慮ERR的情況時,所有的結構都是迴圈,無非有巢狀,巢狀裡還會有並列的,純模擬計算時間複雜度比較麻煩,但是我們發現每次遇到一個E我們肯定要計算之前那個F的複雜度然後看它對答案的貢獻的,一進一出,所以我們要用棧。
每次遇到一次F,就算一下它的複雜度,然後把它入棧。如果遇到E,就把當前棧頂出棧,並且拿它來更新新的棧頂。按照這種思路寫,你每次出棧之後新的棧頂一定是剛出棧的那個迴圈上面套的迴圈,所以更新答案時應該是兩個複雜度相加。
大致思路就是如此,具體細節自己考慮一下。
其實你發現,ERR無非兩種情況:
(1)變數名重複定義 (2)開始迴圈的F與結束迴圈的E對不上
第二種情況在做棧的時候就可以判斷,如果當前top為0,但仍然要出棧,就輸出ERR。
第一種情況可以在記錄棧的同時,記錄一下變數名,用map處理。每次入棧的時候,判斷變數名是否使用,如果使用了就輸出ERR,否則就把變數名加入map,並且記錄一下棧中每個元素對應的變數名,在出棧的時候把map中的變數名再刪去。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;
int t,l,m,top,say,a[210],b[210];
string ch[200];
map<string,bool> mp;
bool flag,tt;
int main(){
//freopen("a.in","r",stdin);
scanf("%d",&t);
while(t--){
int ans=0;
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
mp.clear();
top=0;
scanf("%d",&l);
char s[210];scanf("%s",s+1);
m=strlen(s+1);//cout<<m<<endl;
//for(int i=1;i<=m;i++) cout<<s[i];
//cout<<endl;
flag=0;say=0;tt=0; //flag是判斷有沒有n,say是n的冪 ,tt是看有沒有輸出過
for(int i=1;i<=m;i++){
if(s[i]=='n') flag=1;
if(s[i]>='0'&&s[i]<='9') say=say*10+s[i]-'0';
}
while(l--){
string s1,s2,s3,s4;
int len1=0,len2=0,num1=0,num2=0;bool n1=0,n2=0;
cin>>s1;// cout<<s1<<' ';
if(s1=="F"){
cin>>s2>>s3>>s4; //cout<<s2<<' '<<s3<<' '<<s4<<endl;
if(mp[s2]){
tt=1;
}
mp[s2]=1;
len1=s3.size();len2=s4.size();
for(int i=0;i<len1;i++){
if(s3[i]=='n') n1=1;
if(s3[i]>='0'&&s3[i]<='9') num1=num1*10+s3[i]-'0';
}
for(int i=0;i<len2;i++){
if(s4[i]=='n') n2=1;
if(s4[i]>='0'&&s4[i]<='9') num2=num2*10+s4[i]-'0';
}
top++;
//cout<<"n1:"<<n1<<" "<<"n2:"<<n2<<' '<<"num1:"<<num1<<" num2:"<<num2<<endl;
if(n1&&n2) a[top]=b[top]=0;
else if(n1&&!n2||(num1>num2&&!n1&&!n2)) a[top]=b[top]=-1;
else if(!n1&&n2) a[top]=b[top]=1;
else if(num1<=num2) a[top]=b[top]=0;
//for(int i=1;i<=top;i++) cout<<a[top]<<' '<<b[top]<<endl;
ch[top]=s2;
}else{
if(top==0){
tt=1;continue;
}
if(a[top-1]!=-1){
//cout<<"a[top-1]:"<<a[top-1]<<' '<<"b[top]:"<<b[top]<<endl;
b[top-1]=max(b[top-1],a[top-1]+max(b[top],0));
//cout<<"b[top-1]:"<<b[top-1]<<' '<<"top-1:"<<top-1<<endl;
}
mp[ch[top]]=0;
a[top]=b[top]=0;
top--;
if(top==0) ans=max(ans,b[0]);
}
}//cout<<top<<' '<<b[0]<<' '<<flag<<' '<<say<<endl;
if(tt) printf("ERR\n");
else if(top>0) printf("ERR\n");
else if(b[0]==0&&flag==0) printf("Yes\n");
else if(flag&&ans==say) printf("Yes\n");
else printf("No\n");
}
return 0;
}
五、寶藏
n<=12,顯然是搜尋或者狀壓。我們可以列舉每一個點作為起點,然後dfs,遞迴傳遞的是每一個二進位制壓縮的狀態,再列舉每一個點作為當前所在點,然後再列舉要到達的點,更新最優值。稍微做兩個剪枝,跑的很快。(也可能是資料水?)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 2147483647
int n,m,g[20][20],f[10000],dis[20],ans=INF;
void find(int x)
{
if(f[x]>ans) return ;
for(int i=1;i<=n;i++)
if((1<<(i-1))&x)
{
for(int j=1;j<=n;j++)
if(((1<<(j-1))&x)==0&&g[i][j]!=INF)
{
if(f[x|(1<<(j-1))]>f[x]+dis[i]*g[i][j])
{
int tmp=dis[j];
dis[j]=dis[i]+1;
f[x|(1<<(j-1))]=f[x]+dis[i]*g[i][j];
find(x|(1<<(j-1)));
dis[j]=tmp;
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
g[i][j]=INF;
int u,v,z;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&z);
g[u][v]=min(g[u][v],z);
g[v][u]=min(g[v][u],z);
}
for(int o=1;o<=n;o++)
{
for(int i=1;i<=n;i++) dis[i]=INF;
for(int i=1;i<=(1<<n)-1;i++) f[i]=INF;
dis[o]=1;
f[1<<(o-1)]=0;
find(1<<(o-1));
ans=min(ans,f[(1<<n)-1]);
}
printf("%d",ans);
return 0;
}
六、列隊
我們發現,每一次出去一個人之後,改變的位置只有當前所在行,以及最後一列。考慮我們其實每次修改一次,可以把原來的那個點刪除,然後分別在行、列的末尾新加入一個點,然後每次查詢我們要做的是求區間第k個數。這個可以用線段樹實現。我們開n+1棵線段樹,前n個分別維護的是矩陣的n行的前m-1的人編號的情況,第n+1棵維護的是最後一列的人的編號情況。每次要求x,y位置人的編號,如果y=m,我們就是在第n+1棵線段樹上找第x個數的val。否則就是在第x棵線段樹上找第y個數的val;修改類似。當然,開n+1棵線段樹,複雜度肯定爆炸啊,所以我們要動態開點。首先,線段樹維護的區間長度是多少呢?我們發現,初始長度為max(n,m),最多加了q個點,那麼最大長度p就是max(n,m)+q;一般動態開點,線段樹的空間要p*20.不過已經符合了本題的要求。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e5+10;
int n,m,q,p,tot,now;
int ls[maxn*20],rs[maxn*20],root[maxn],siz[maxn*20],pos[maxn];
ll val[maxn*20];
int read(){
char ch=getchar();int num=0,f=1;
while(!isdigit(ch)){if(ch=='-') f=-1; ch=getchar();}
while(isdigit(ch)){num=num*10+ch-'0'; ch=getchar();}
return num*f;
}
void init(){
n=read();m=read();q=read();
}
int calc(int l,int r){//計算這一段區間初始有多少個數
if(now==n+1){
if(r<=n) return r-l+1;
else if(l<=n) return n-l+1;
else return 0;
}
if(r<m) return r-l+1;
else if(l<m) return m-l;
else return 0;
}
ll query(int &id,int l,int r,int x){
if(!id){//若當前節點沒有開
id=++tot;//新開一個節點
siz[id]=calc(l,r);//計算當前節點的數的個數
if(l==r) {//如果是葉子節點
if(now<=n) val[id]= (ll)m*(now-1)+1LL*l;
else val[id]= (ll)l*m;//計算節點編號
}
}
siz[id]--;//將一個點去除
if(l==r) return val[id];
int mid=l+r>>1;
if(!ls[id]&&mid-l+1>=x) return query(ls[id],l,mid,x);
if(ls[id]&&siz[ls[id]]>=x) return query(ls[id],l,mid,x);
if(ls[id]) x-=siz[ls[id]];
else x-=mid-l+1;
return query(rs[id],mid+1,r,x);
}
void update(int &id,int l,int r,ll temp,int x){
if(!id){
id=++tot;
siz[id]=calc(l,r);
if(l==r){
val[id]=temp;
}
}
siz[id]++;//新增一個節點
if(l==r) return ;
int mid=l+r>>1;
if(mid>=x) update(ls[id],l,mid,temp,x);
else update(rs[id],mid+1,r,temp,x);
}
void work(){
ll ans;p=max(m,n)+q;
for(int i=1;i<=q;i++){
int x,y;scanf("%d%d",&x,&y);
now=x;
if(y==m){
now=n+1;
ans=query(root[now],1,p,x);
}else
ans=query(root[now],1,p,y);
printf("%lld\n",ans);
now=n+1;pos[now]++;
update(root[now],1,p,ans,n+pos[now]);
if(y^m){
ans=query(root[now],1,p,x);
now=x;++pos[now];
update(root[now],1,p,ans,m-1+pos[now]);
}
}
}
int main(){
init();
work();
return 0;
}