散知識點總結(持更)

Redamancy_Lydic發表於2024-08-10

有一些小 trick,專門用一整篇部落格來寫不太合適,所以都放在這裡吧。

逆序對

考試的時候樹狀陣列做法顯然比其他的都好寫。

考慮每個元素對答案的貢獻,我們需要知道在它之前有多少元素比它大。

我們只需要維護一個權值樹狀陣列,在列舉到 \(i\) 的時候查詢當前樹狀陣列中的元素有多少比它大,為了方便處理我們倒序列舉,這樣相當於求順序對,方便樹狀陣列計算答案。

每次累加以後只需要把當前元素加進樹狀陣列即可。

for(int i=n;i>=1;i--) 
{
	ans+=ask(a[i]-1);	
	add(a[i],1);
}

二進位制分組

如果我們有一堆數字,現在要把它們多次分成兩組,要求對於每兩個數字都至少有一次分在不同的組。

我們考慮二進位制,對於每一位,把所有數字這一位為 \(0\) 的分成一組,為 \(1\) 的分成一組,可以證明這樣一定能滿足上述條件。

簡單證明

對於兩個不同的數字 \(i,j\),它們的二進位制表達一定至少有一位是不同的,。因此按照上述分組方法一定會至少有一次不在同一組裡面。

證畢。

分組的過程很簡單,所以給一道例題。

[GXOI/GZOI2019] 旅行者

把所有感興趣的城市進行二進位制分組,然後搞兩個虛擬點,一個起點一個終點,分別給兩組點連 \(0\) 邊權的邊,然後跑最短路即可。

可愛的程式碼
#include<bits/stdc++.h>
#define int long long
inline int read()
{
	int w=1,s=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch)){s=(s<<1)+(s<<3)+(ch^48);ch=getchar();}
	return w*s;
} 
using namespace std;
const int maxn=1e6+10;
int T,n,m,k;
int a[maxn];
struct no
{
	int y,v;
};
vector<no> G[maxn];
struct dij
{
	int y,id;
	inline friend bool operator < (dij x,dij y)
	{
		return x.y>y.y;
	}
};
int dis[maxn];
bool vis[maxn];
void dijkstra()
{
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	priority_queue<dij> q;
	dis[0]=0;
	q.push({0,0});
	while(!q.empty())
	{
		int u=q.top().id;
		q.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(auto i : G[u])
		{
			int y=i.y,v=i.v;
			if(dis[u]+v<dis[y])
			{
				dis[y]=dis[u]+v;
				if(!vis[y])
				q.push({dis[y],y});
			}
		}
	}
}
signed main()
{
//	freopen("tourist.in","r",stdin);
//	freopen("tourist.out","w",stdout);
	cin>>T;
	while(T--)
	{
		n=read(),m=read(),k=read();
		for(int i=0;i<=n;i++)G[i].clear();
		for(int i=1;i<=m;i++)
		{
			int x=read(),y=read(),v=read();
			G[x].push_back({y,v});
		}
		for(int i=1;i<=k;i++)a[i]=read();
		int ans=0x3f3f3f3f3f3f;
		for(int i=0;(1<<i)<=n;i++)
		{
			G[0].clear();
			for(int j=1;j<=k;j++)
			{
	            if(a[j]&(1<<i))G[0].push_back({a[j],0});
	            else G[a[j]].push_back({n+1,0});
        	}
        	dijkstra();
        	ans=min(ans,dis[n+1]);
        	for(int j=1;j<=k;j++)
            if((!(a[j]&(1<<i))))G[a[j]].pop_back();
	        G[0].clear();
	        for(int j=1;j<=k;j++)
			{
	            if(!(a[j]&(1<<i)))G[0].push_back({a[j],0});
	            else G[a[j]].push_back({n+1,0});
	    	}
	        dijkstra();
	        ans=min(ans,dis[n+1]);
	        for(int j=1;j<=k;j++)
	        if((a[j]&(1<<i))) G[a[j]].pop_back();      
		}
		printf("%lld\n",ans);
	}
	return 0;
}

其實簡單一想就能發現類似的還有三進位制四進位制之類的分組,不過這些也不常用,但知道一點是一點。

高維字首和

這個不是樹狀陣列區間操作的高維字首和(這個誰用啊),而是維度。

類比ABC366D這道題,先給出一到三維字首和陣列的計算方法:

\[sum_i=a_i+sum_{i-1} \]

\[sum_{i,j}=a_{i,j}+sum_{i-1,j}+sum_{i,j-1}-sum_{i-1,j-1} \]

\[sum_{i,j,k}=a_{i,j,k}+sum_{i,j,k-1}+sum_{i,j-1,k}+sum_{i-1,j,k}-sum_{i,j-1,k-1}-sum_{i-1,j,k-1}-sum_{i-1,j-1,k}+sum_{i-1,j-1,k-1} \]

程式碼版(複製用)
sum[i]=a[i]+sum[i-1];
sum[i][j]=a[i][j]+sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1];
sum[i][j][k]=a[i][j][k]+sum[i][j][k-1]+sum[i][j-1][k]+sum[i-1][j][k]-sum[i][j-1][k-1]-sum[i-1][j][k-1]-sum[i-1][j-1][k]+sum[i-1][j-1][k-1];

稍微懂一點數學的人都已經能看出來規律了。進行 \(n\) 維字首和的時候,對於字首和陣列的每一維,我們分別對 \(1\sim n\) 個下標進行 \(-1\) 的修改,如果修改個數維奇數,則符號為加號,否則為減號。

容易發現若修改個數為 \(m\),則共需要修改 \(n\choose m\) 次。因此太高維的一般也不常用,瞭解即可。

相關文章