做题笔记(三)

[USACO13DEC] Vacation Planning G

题目传送门

[USACO13DEC] Vacation Planning G

思路

总结:一点点小思维+最短路板子。

显然我们发现,对于每一次询问的出发点 \(u\) 只有两种可能:是中枢或者不是中枢。

同时注意到,\(K\) 的范围非常小,所以可以考虑在中枢上做文章。我们可以先试着在 \(K\) 个中枢中跑 \(K\) 遍最短路。然后分类讨论(假定中枢点集合为 \(S\)):

  • \(u\in S\):显然我们已经处理过了 \(u\) 的最短路,所以我们可以直接判断 \(u\) 是否可以到 \(v\) 并且很快查询其花费。

  • \(u\notin S\):根据题目性质可以得出每一个与 \(u\) 相连的节点 \(w\) 都满足 \(w\in S\),而每一个 \(w\) 的最短路也已知,所以可以遍历一遍 \(u\) 所能到达的点再查询花费。

注意到每一个中枢的范围在 \([1,N]\),似乎二维的最短路数组存不下,那就先给它离散化一下吧,这样每一个中枢的范围就控制在了 \([1,K]\)

似乎 SPFA 都可以跑过去,但我们还是选择 Dijkstra。

最坏时间复杂度为 \(O(KN\log(N+M)+KQ)\)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
    int x=0,f=1;
    char c=getchar();

    for(;c<'0'||c>'9';c=getchar())
        if(c=='-') f=-1;

    for(;c>='0'&&c<='9';c=getchar())
        x=x*10+c-'0';

    return x*f;
}

inline void Write(int x){
    if(x<0){
        putchar('-');
        x=-x;
    }

    if(x>9) Write(x/10);

    putchar(x%10+'0');
}

inline void write(int x){
    Write(x);
    putchar('\n');
}

const int MAXN=20005,MAXK=205;
int n,m,k,q;
vector<pair<int,int> >V[MAXN];
int hub[MAXK],id[MAXN]={0},cnt=0;
int dis[MAXK][MAXN],tot=0,ans=0;
bool vis[MAXN]={0},ishub[MAXN]={0};

void dijkstra(int s){
	for(int i=1;i<=n;i++)
		vis[i]=0;
	
	priority_queue<pair<int,int> >q;
	dis[id[s]][s]=0;
	q.push(make_pair(0,s));
	
	while(!q.empty()){
		int t=q.top().second;
		q.pop();
		
		if(vis[t]) continue;
		else vis[t]=1;
		
		for(int i=0;i<V[t].size();i++){
			int to=V[t][i].first;
			int w=V[t][i].second;
			
			if(dis[id[s]][to]>dis[id[s]][t]+w){
				dis[id[s]][to]=dis[id[s]][t]+w;
				q.push(make_pair(-dis[id[s]][to],to));
			}
		}
	}
}

signed main(){
	n=read(),m=read();
	k=read(),q=read();
	
	for(int i=1;i<=k;i++)
		for(int j=1;j<=n;j++)
			dis[i][j]=1e17;
	
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		V[u].push_back(make_pair(v,w));
	}
	
	for(int i=1;i<=k;i++){
		hub[i]=read();
		ishub[hub[i]]=1;
		id[hub[i]]=(++cnt);
	}
	
	for(int i=1;i<=k;i++)
		dijkstra(hub[i]);
		
	while(q--){
		int u=read(),v=read();
		
		if(ishub[u]){
			if(dis[id[u]][v]!=1e17){
				tot++;
				ans+=dis[id[u]][v];
			}
		}
		else{
			int mn=1e17;
			
			for(int i=0;i<V[u].size();i++){
				int to=V[u][i].first;
				int w=V[u][i].second;
				mn=min(dis[id[to]][v]+w,mn);
			}
			
			if(mn!=1e17){
				tot++;
				ans+=mn;
			}
		}
	}
	
	write(tot);
	write(ans);

    return 0;
}

【模板】卢卡斯定理/Lucas 定理

题目传送门

【模板】卢卡斯定理/Lucas 定理

思路

今天学的一个小定理:

\[\dbinom{n}{m}\mod p=\dbinom{\lfloor\frac{n}{p}\rfloor}{\lfloor\frac{m}{p}\rfloor}\cdot\dbinom{n\mod p}{m\mod p}\mod p \]

主要用于求解大组合数取模大质数问题。

附:

  • 威尔逊定理:\((p-1)!\equiv-1(\mod p)\)

  • 费马小定理:\(a^{p-1}\equiv1(\mod p)\)

  • 裴蜀定理:\(\sum_ix_ia_i=d\) 成立当且仅当 \(\gcd(a_{1\rightarrow n})\mid d\)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int MAXP=1e5+5;
int n,m,p=1e9+7,lc[MAXP]={1};

int qpow(int a,int b){
	int res=1;
	
	while(b){
		if(b&1) res=((res%p)*(a%p))%p;

		a=((a%p)*(a%p))%p;
		b>>=1;
	}
	
	return res;
}

int getC(int n,int m){
	if(m>n) return 0;
	
	int a=lc[n];
	int b=qpow(lc[m],p-2);
	int c=qpow(lc[n-m],p-2);
	
	return (a*b%p*c%p);
}

int lucas(int n,int m){
	if(m==0) return 1;
	
	int a=getC(n%p,m%p);
	int b=lucas(n/p,m/p);
	return a*b%p;
}

void solve(){
	scanf("%lld%lld%lld",&n,&m,&p);
	
	for(int i=1;i<=p;i++)
		lc[i]=(lc[i-1]*i)%p;
		
	printf("%lld\n",lucas(n+m,n));
}

signed main(){
	
	int t; scanf("%lld",&t);
	
	while(t--) solve();

    return 0;
}

[GESP202312 八级] 奖品分配

题目传送门

[GESP202312 八级] 奖品分配

思路

组合数学基础题。

首先,面对组合数学,我们需要学会转化问题。首先,我们可以把这个问题转化为:有 \(N\) 个盒子,\(M\) 种物品,每种物品有 \(a_i\) 个,填入这 \(N\) 个盒子中的方案数。

这类填坑问题一般以物品为切入点。考虑第 \(i\) 个物品,假设剩余了 \(K\) 个坑,那么其贡献的方案因子为 \(\dbinom{K}{a_i}\),填完之后剩余的坑的数量就是 \(K-a_i\) 个。

然后根据乘法原理即可,时间复杂度 \(O(T(N+M))\)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int MOD=1e9+7;
int n,m,a[1005],C[2005][2005]={0};//n,m

void getC(){
	for(int i=0;i<=2000;i++)
		C[i][0]=1;
		
	for(int i=1;i<=2000;i++){
		for(int j=1;j<=i;j++){
			C[i][j]=C[i-1][j]+C[i-1][j-1];
			C[i][j]%=MOD;
		}
	}
}

void solve(){
	int tot=0,res=1;
	scanf("%lld%lld",&n,&m);
	
	for(int i=1;i<=m;i++){
		scanf("%lld",&a[i]);
		tot+=a[i];
	}
	
	if(tot!=n) n++;
	
	for(int i=1;i<=m;i++){
		if(a[i]!=0){
			res*=C[n][a[i]];
			res%=MOD;
			n-=a[i];
		}
	}
	
	printf("%lld\n",res);
}

signed main(){
	
	getC();
	int t; scanf("%lld",&t);
	
	while(t--) solve();

    return 0;
}

[GESP202403 八级] 公倍数问题

题目传送门

[GESP202403 八级] 公倍数问题

思路

假设一个数 \(a\) 的因子个数是 \(b\),那么显然 \(a\)\(b^2\) 个有序数对的公倍数。所以这道题的难度在于预处理 \(1\rightarrow K\) 的不大于 \(N\)\(M\) 的因子个数。

显然有一种方法:对于一个数 \(i\),那么它一定是 \(i,i+i,i+i+i\cdots\) 的因子。

那么这个方法的整体复杂度就是 \(O(\sum^N\lfloor\frac{K}{i}\rfloor+\sum^M\lfloor\frac{K}{i}\rfloor+K)\)

我们分析一下复杂度:显然前面的两个 sigma 代表的是两个调和级数求和。直接分析的话还是非常有难度的,因为它是发散的,所以我们可以用一个程序来计算其语句进行次数。事实证明,在 \(N\)\(K\) 等价并且 \(N\approx10^{k},4\le k\le6\) 时,其运行次数总是小于 \(N\)\(\frac{3}{2}\),即:\(\sum^N\lfloor\frac{N}{i}\rfloor<1.5N\),所以可以通过 1ms 的时间限制。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int MAXK=1e6+5;
int n,m,k,inn[MAXK]={0},inm[MAXK]={0},tot=0;

signed main(){
	
	scanf("%lld%lld%lld",&n,&m,&k);
	
	for(int i=1;i<=n;i++){
		for(int j=i;j<=k;j+=i)
			inn[j]++;
	}
	
	for(int i=1;i<=m;i++){
		for(int j=i;j<=k;j+=i)
			inm[j]++;
	}
	
	for(int i=1;i<=k;i++)
		tot+=(i*inn[i]*inm[i]);
		
	printf("%lld\n",tot);

    return 0;
}
posted @ 2024-08-12 20:23  SnapYust  阅读(23)  评论(0)    收藏  举报