1. 程式人生 > 其它 >6.20考試總結(NOIP模擬9)[斐波那契·數顏色·分組]

6.20考試總結(NOIP模擬9)[斐波那契·數顏色·分組]

T1 斐波那契

解題思路

題目傳送門

\(70pts\)做法

這個做法比較暴力,考場上也是看到範圍\(10^{12}\)後知道需要推式子,但是感覺自己太菜了,沒敢輕易嘗試,然後就去搞\(10^6\)的部分分去了。。。

比較好理解,直接暴力建樹(其實就是找到每個節點的父親節點),然後直接在樹上搞 LCA 就好了,程式碼實現上也沒有什麼太大問題。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e7+10,M=N<<1;
int n,m,cnt=1,las=1,f[N][25],dep[N];
struct Ques
{
	int a,b;
}q[N];
inline void build()
{
	while(cnt<n)
	{
		int temp=las;
		las=cnt;
		for(int i=1;i<=temp;i++)
		{
			f[++cnt][0]=i;
			dep[cnt]=dep[i]+1;
			if(cnt>=n)
				break;
		}
	}
}
inline void LCA_init()
{
	f[1][0]=1;
	for(int j=1;j<=20;j++)
		for(int i=1;i<=n;i++)
			f[i][j]=f[f[i][j-1]][j-1];
}
int LCA_ask(int x,int y)
{
	if(x==y)
		return x;
	if(dep[x]>dep[y])
		swap(x,y);
	for(int i=20;i>=0;i--)
		if(dep[x]<=dep[f[y][i]])
			y=f[y][i];
	if(x==y)
		return x;
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	return f[x][0];
}
int main()
{
//	freopen("fibonacci2.in","r",stdin);
//	freopen("fibonacci2.out","w",stdout);
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&q[i].a,&q[i].b);
		n=max(n,max(q[i].a,q[i].b));
	}
	build();
	LCA_init();
	/*
 	for(int i=1;i<=n;i++)
	{
		cout<<i<<"To:      ";
		for(int j=head[i];j;j=nxt[j])
			cout<<ver[j]<<' ';
		cout<<endl;
	}
	*/
	for(int i=1;i<=m;i++)
		printf("%d\n",LCA_ask(q[i].a,q[i].b));
	return 0;
}

\(100pts\) 做法

仔細觀察序列,可以發現第i個月出生的第j只兔子的編號是\(f(i-1)+j\)父親就是j,顯然\(j<f(i-1)\),然後移一下項:

\[令f(i)<x\le f(i+1),有father(x)=x-f(i) \]

然後我們再斐波那契數列上二分(用lower_bound實現)。

對於每一個詢問,選擇編號大的向上跳父親節點,直到找到LCA為止(記得開long long)。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4,M=3e5+50;
int n,m,cnt=2,f[N];
struct Ques
{
	int a,b;
}q[M];
#undef int
int main()
{
//	#define int register long long
	#define int long long
	scanf("%lld",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld",&q[i].a,&q[i].b);
		n=max(n,max(q[i].a,q[i].b));
	}
	f[1]=f[2]=1;
	for(int i=3;i<=N;i++)
	{
		f[i]=f[i-1]+f[i-2];
		if(f[i]>=n)
		{
			cnt=i;
			break;
		}
	}
	for(int i=1;i<=m;i++)
	{
		int a=q[i].a,b=q[i].b;
		while(a!=b)
		{
			if(a<b)
				a^=b^=a^=b;
			int num=lower_bound(f+1,f+cnt+1,a)-f-1;
			int temp=max(1ll,num);
			a-=f[temp];
		}
		printf("%lld\n",a);
	}
	return 0;
}

T2 數顏色

解題思路

本題做法有很多,也有許多非常好的方法,諸如線段樹,主席樹,二分之類的;

這裡主要說一下來自wtz大佬的分塊做法:

很簡單,對於每一個塊內的不同顏色分別計算個數,更改的時候直接更改個數,以及原序列就好了。

需要注意的就是每個塊的大小 \(\dfrac{1}{2}\)次方會MLE ,\(\dfrac{2}{3}\) 次方會TLE,因此我們有了。。。

\[\huge0.625次方yyds \]

於是我們愉快地幹掉了此題。當然似乎別的次方也可以,可以自行嘗試。。。

code

線段樹

來自wzr

主席樹

來自zxb

二分

來自pyt

分塊

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
int n,m,len,tot,pos[N],s[N],sum[N][115],li[N],ri[N];
inline int ask(int l,int r,int col)
{
	int p=pos[l],q=pos[r],ans=0;
	if(p==q)
	{
		for(int i=l;i<=r;i++)
			ans+=(s[i]==col);
		return ans;
	}
	for(int i=p+1;i<=q-1;i++)
		ans+=sum[col][i];
	for(int i=l;i<=ri[p];i++)
		ans+=(s[i]==col);
	for(int i=li[q];i<=r;i++)
		ans+=(s[i]==col);
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	len=pow(n,0.625);
	tot=n/len;
//	cout<<tot<<endl;
	for(int i=1;i<=n;i++)
		scanf("%d",&s[i]);
	for(int i=1;i<=tot;i++)
	{
		li[i]=(i-1)*len+1;
		ri[i]=i*len;
	}
	if(ri[tot]<n)
	{
		tot++;
		li[tot]=ri[tot-1]+1;
		ri[tot]=n;
	}
	for(int i=1;i<=tot;i++)
		for(int j=li[i];j<=ri[i];j++)
		{
			pos[j]=i;
			sum[s[j]][i]++;
		}
	for(int i=1,l,r,col,x,opt;i<=m;i++)
	{
		scanf("%d",&opt);
		if(opt==1)
		{
			scanf("%d%d%d",&l,&r,&col);
			printf("%d\n",ask(l,r,col));
		}
		else
		{
			scanf("%d",&x);
			if(pos[x]!=pos[x+1])
			{
				sum[s[x]][pos[x]]--;
				sum[s[x+1]][pos[x+1]]--;
				sum[s[x]][pos[x+1]]++;
				sum[s[x+1]][pos[x]]++;
			}
			swap(s[x],s[x+1]);
		}
	}	
	return 0;
}

T3 分組

推銷

解題思路

參考了題解區一篇思路非常好的題解,在這裡講一下自己的見解。

首先明確一下 K 的取值只有 1 或者 2 這裡看資料範圍非常重要!,對於 \(K=1\)\(K=2\) 的情況要分開來做。

K=1

對於 \(K=1\) 的情況,為了保證字典序最小,我們需要倒著列舉序列了。

然後再次觀察資料範圍,發現\(131072 \times2=512^2\),因此我們可以列舉 \(1 \sim 512\) ,令 vis[i] 表示在當前掃到的組裡顏色為 i 的是否存在,檢視是否訪問過 \(x^2-s_i\)

  • 如果訪問過,表示和第 i 只兔子發生矛盾的已經在這個組裡了,因此需要再次分一個組,並且記錄下分組的邊界,清空 vis 陣列。

  • 如果沒有訪問過,把該種顏色的標記成 true 記錄就好了。

K=2

幾乎同樣的思路,我們仍然需要倒著列舉序列。

對於同一組的兔子,狀態之可能有兩種:同一小團體或者在敵對小團體,因此我們用並查集維護。

  • \(\operatorname{find}(1 \sim n)\) 表示 \(1\sim n\)的兔子所在的小團體。

  • \(\operatorname{find}(n+1 \sim 2 \times n)\) 表示 \(1\sim n\)的兔子所在的小團體的敵對小團體。

然後開一個 vector 陣列記錄同一顏色的序號,然後分別對於發生矛盾的兔子進行判斷,同時更新該兔子所在組以及小團體和敵對小團體。

同樣的,如果矛盾無法避免,那就重新開一個組,並清空標記,記錄分割點就好了。

  • 注意:並查集合並時要在 find 的基礎上更新

code

#include<bits/stdc++.h>
using namespace std;
const int M=131080;
int n,m,K,las,s[M],fa[M<<1];
vector<int> ans,v[M<<1];
bool vis[M];
int find(int x)
{
	if(fa[x]==x)
		return x;
	return fa[x]=find(fa[x]);
}
void work_1()
{
	for(int i=n;i>=1;i--)
	{
		bool flag=true;
		for(int j=1;j<=512;j++)
			if(j*j>=s[i])
				if(vis[j*j-s[i]])
				{
					flag=false;
					break;
				}
		if(!flag)
		{
			for(int j=i+1;j<las;j++)
				vis[s[j]]=false;
			ans.push_back(i);
			las=i+1;
		}
		vis[s[i]]=true;
	}
	printf("%d\n",ans.size()+1);
	for(int i=ans.size()-1;i>=0;i--)
		printf("%d ",ans[i]);
}
int update(int l,int r)
{
	for(int i=l+1;i<r;i++)
		vector <int>().swap(v[s[i]]);
	ans.push_back(l);
	return l+1;
}
void work_2()
{
	for(int i=1;i<=(n<<1);i++)
		fa[i]=i;
	for(int i=n;i>=1;i--)
	{
		for(int j=1;j<=512;j++)
			if(j*j>=s[i])
				if(v[j*j-s[i]].size())
					for(int k=0;k<v[j*j-s[i]].size();k++)
					{
						int temp=v[j*j-s[i]][k];
						if(find(temp)==find(i))
						{
							las=update(i,las);
							break;
						}
						else
						{
							fa[find(i+n)]=find(temp);
							fa[find(temp+n)]=find(i);
						}
					}
		v[s[i]].push_back(i);
	}
	printf("%d\n",ans.size()+1);
	for(int i=ans.size()-1;i>=0;i--)
		printf("%d ",ans[i]);
}
int main()
{
	scanf("%d%d",&n,&K);
	las=n+1;
	for(int i=1;i<=n;i++)
		scanf("%d",&s[i]);
	if(K==1)
		work_1();
	else
		work_2();
	return 0;
}