题目整理-5(练习4)

T1释放囚犯

题面描述:

\(n\) 间房子,每个房子中都有一个囚犯,房子按照标号排成一列,同时,相邻的两个房子的人可以传话到更远的房子。
现在,需要释放 \(m\) 个囚犯,标号从 \(a_1\)\(a_m\) ,在释放一个囚犯时,需要给原本可以和他说上话的人一人一份肉吃。
求发的肉的最少份数。

思路:

在释放一个囚犯后,他的左右两边的人的释放顺序就互不影响了。
因此,我们可以轻松想到区间 \(dp\),但是由于细节不好处理,本人选择了记忆化搜索。

标签:

区间dp

代码实现:

this
#include<bits/stdc++.h>
#define N 105
#define INF 0x3f3f3f3f
using namespace std;
int n,m,a[N];
int ans=INF,f[N][N];
int dfs(int i,int j){
	if(f[i][j]!=INF) return f[i][j];
	if(i==j) return (f[i][j]=a[j+1]-a[i-1]-2);
	f[i][j]=min(dfs(i+1,j),dfs(i,j-1));
	for(int k=i+1;k<j;k++){
		f[i][j]=min(f[i][j],dfs(i,k-1)+dfs(k+1,j));
	}
	f[i][j]+=a[j+1]-a[i-1]-2;
	return f[i][j];
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++) cin>>a[i];
	a[m+1]=n+1;
	memset(f,0x3f,sizeof(f));
	cout<<dfs(1,m);
	return 0;
}

T2色板游戏

题面描述:

给你一个初始都为 \(1\) 的数列,有两个操作如下:

  1. \([i,j]\) 区间内的数变为 \(k\)
  2. 统计区间 \([i,j]\) 有多少种数

思路:

观察颜色数量发现,颜色数最大只有 \(30\)!因此,我们可以对颜色进行状压,配合线段树高效解决此题。

标签:

线段树
状态压缩

代码实现:

this
#include<bits/stdc++.h>
#define N 100005
#define lowbit(x) (x&(-x))
#define il inline
using namespace std;
template<class T,int count>
struct x_tree{
	x_tree<T,count-1> s[2];
	#define mid (1<<(count-1))
	#define len (1<<count)
	T sm,lz;
	il x_tree(){sm=lz=0;}
	il void x_add(T k){sm=lz=k;}
	il void push_up(){sm=s[0].sm|s[1].sm;}
	il void push_down(){
		if(!lz) return;
		s[0].x_add(lz);
		s[1].x_add(lz);
		lz=0;
	}
	il void add(int l,int r,T k){
		if(l<=1&&len<=r) x_add(k);
		else{
			push_down();
			if(mid>=l) s[0].add(l,r,k);
			if(mid<r) s[1].add(l-mid,r-mid,k);
			push_up();
		}
	}
	il T sum(int l,int r){
		if(l<=1&&len<=r) return sm;
		push_down();
		T ret=0;
		if(mid>=l) ret|=s[0].sum(l,r);
		if(mid<r) ret|=s[1].sum(l-mid,r-mid);
		return ret;
	}
	#undef len
	#undef mid
};
template<class T>
struct x_tree<T,0>{
	T sm;
	il x_tree(){sm=0;}
	il void x_add(T k){sm=k;}
	il void add(int l,int r,T k){sm=k;}
	il T sum(int l,int r){return sm;}
};
x_tree<int,17> t;
int n,m,T;
char c;
int num_2(int k){
	int ret=0;
	while(k) ret++,k-=lowbit(k);
	return ret;
}
int main(){
	cin>>n>>m>>T;
	t.add(1,n,1);
	while(T--){
		int l,r,k;
		cin>>c>>l>>r;
		if(l>r) swap(l,r);
		if(c=='C'){
			cin>>k;
			t.add(l,r,1<<(k-1));
		}
		else cout<<num_2(t.sum(l,r))<<"\n";
	}
	return 0;
}

T3小 a 和 uim 之大逃离

题面描述:

有一个 \(n\times m\) 的巨幅矩阵,矩阵的每个格子上有一坨 \(0\sim k\) 不等量的魔液。

小 a 和 uim 各有一个魔瓶,他们可以从矩阵的任一个格子开始,每次向右或向下走一步,从任一个格子结束。开始时小 a 用魔瓶吸收地面上的魔液,下一步由 uim 吸收,如此交替下去,并且要求最后一步必须由 uim 吸收。魔瓶只有 \(k\) 的容量,也就是说,如果装了 \(k+1\) 那么魔瓶会被清空成零,如果装了 \(k+2\) 就只剩下 \(1\),依次类推。

请你统计走到最后一步,他俩的魔瓶中魔液一样多有多少种方法。由于可能很大,输出对 \(1,000,000,007\) 取余后的结果。

思路:

非常典型的坐标 \(dp\)
\(f0_{i,j,k_1,k_2}\)\(f1_{i,j,k_1,k_2}\) 分别表示走到了 \((i,j)\) 时,小 a 和 uim 分别有 \(k_1\)\(k_2\) 的魔液,且最后一步是 uim 走的的方案数,易得状态转移方程。同时注意常数即可。
具体看代码。

标签:

坐标dp

代码实现:

this
#include<bits/stdc++.h>
#define N 805
#define mod 1000000007
using namespace std;
int n,m,k,a[N][N];
int ans,f0[2][N][16][16],f1[2][N][16][16];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) cin>>a[i][j];
	int t1,t2;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			++f1[i&1][j][a[i][j]][0]==mod?f1[i&1][j][a[i][j]][0]=0:0;//滚动数组预处理
			for(int k1=0;k1<=k;k1++){
				(ans+=f0[i&1][j][k1][k1])%=mod;//在滚动前统计答案
				for(int k2=0;k2<=k;k2++){
					t1=k1+a[i][j+1],t2=k2+a[i][j+1];//提前计算,节省时间
					t1>k?t1-=k+1:0;
					t2>k?t2-=k+1:0;
					(f1[i&1][j+1][t1][k2]+=f0[i&1][j][k1][k2])%=mod;//状态转移
					(f0[i&1][j+1][k1][t2]+=f1[i&1][j][k1][k2])%=mod;//状态转移
					t1=k1+a[i+1][j],t2=k2+a[i+1][j];//提前计算,节省时间
					t1>k?t1-=k+1:0;
					t2>k?t2-=k+1:0;
					(f1[(i&1)^1][j][t1][k2]+=f0[i&1][j][k1][k2])%=mod;//状态转移
					(f0[(i&1)^1][j][k1][t2]+=f1[i&1][j][k1][k2])%=mod;//状态转移
					f0[i&1][j][k1][k2]=f1[i&1][j][k1][k2]=0;//滚动数组,节省空间
				}
			}
		}
	}
	cout<<ans<<"\n";
	return 0;
}

T4树的重量

题面描述:

给出你一个边权全非负的无根树上所有叶子节点之间的距离,求这棵树的边权之和。
同时,保证 \(dis_{i,j}+dis_{j,k}\geq dis_{i,k}\)

思路:

先说一个结论:在这个无根树中,从任意一个叶节点出发,经过所有的叶节点并回到该节点,最短路径为这棵树的边权和的两倍,即,会经过所有的边恰好两次。
接下来不严谨证明一下。

考虑将这颗无根树以一个非叶子节点为根,使其为一颗有根树。接下来,从画出的最左边出发,依次向右走到下一个节点,举个例子如下:

\(5\) 出发,依次走最短路到 \(6,10,3,8,10\),最后再从 \(10\) 走最短路回到 \(5\) ,易发现,每一条边都经过了两遍。
易得,这种方法走为最优解。

得到了这个结论后,结合样例并思考得:这不就是给出一个 \(n\) 个点的无向图,求出经过 \(n\) 个点得最小环吗?因此,完结撒花......就怪了。
我们还不清楚如何求出这个环。结合一定思考并结合性质,可得:
假设此时已经处理出了经过前 \(k\) 个点的最小环,那么可以枚举之前加入环的边,将其=断开,并将原本这条边的两个端点分别连接到 \(k+1\) 上,找到增加最小的一条边并断开连接,一直向下处理即可。
真的易证
代码实践不难。

标签:

贪心
最小环

代码实现:

this
#include<bits/stdc++.h>
#define N 35
using namespace std;
int n,a[N][N],vis[N][N],ans;
int main(){
	cin>>n;
	for(int i=1;i<n;i++)
		for(int j=i+1;j<=n;j++) cin>>a[i][j];
	ans+=a[1][2]+a[2][3]+a[1][3];
	vis[1][2]=vis[2][3]=vis[1][3]=1;
	for(int k=4;k<=n;k++){
		int t1=0,t2=0;
		for(int i=1;i<k;i++)
			for(int j=i+1;j<k;j++){
				if((vis[i][j]&&a[i][k]+a[j][k]-a[i][j]<a[t1][k]+a[t2][k]-a[t1][t2])||t1==0) t1=i,t2=j;
			}
		vis[t1][t2]=0,vis[t1][k]=vis[t2][k]=1;
		ans+=a[t1][k]+a[t2][k]-a[t1][t2];
	}
	cout<<ans/2;
	return 0;
}

T5[ROIR 2022] 分数排序 (Day 2)

题面描述:

有两个由 \(n\) 个不同整数组成的序列 \(A = [a_1, a_2, \dots , a_n]\)\(B = [b_1, b_2, \dots , b_n]\)。将它们组合成 \(n^2\) 个分数,形式为 \(\frac{a_i}{b_j}\),并将每个分数约分后按递增顺序排序。

给定一个数字 \(q\)\(q\) 个整数 \(c_1, c_2, \dots , c_q\)。对于每个 \(c_i\),请输出上面所说的 \(n^2\) 个分数中第 \(c_i\) 小的分数。

思路:

先将 \(a\)\(b\) 排序。
可以发现,当这个分数的值越大,它的排名也越大(这不是废话吗),同时,求一个分数的排名也十分容易,只需要计算出对于每个 \(b_j\) 有多少个 \(\frac{a_i}{b_j}\) 比它小就好了(不过需要注意的是,可能有多个相同值的分数,事实上我们求出来的只能是一个排名区间),而且时间复杂度也很优,\(q\)次的提问复杂度仅为 \(qn\log_2(n)\)(因为 \(qn<10^5\)),所以,结合单调性,我们通过实数二分得到排名 \(c_i\) 的数。
接下来就简单了,我们只需要枚举每个 \(b_j\) 二分找出满足要求的 \(a_i\),最后取所有答案中误差最小的的即可。

标签:

实数二分
二分

代码实现:

this
#include<bits/stdc++.h>
#define N 100005
#define ll long long
#define eps 1e-12
using namespace std;
ll n,q,a[N],b[N],c; 
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
double jue(double x){return x>=0?x:-x;}
ll ck(double md){
	ll ln=0,rn=0,l=1,r=n;
	for(ll j=1;j<=n;j++){
		r=n;
		while(l<r){
			ll mid=(l+r)/2;
			(a[mid]*1.0>md*b[j])?r=mid:l=mid+1;
		}
		if(a[l-1]*1.0<=md*b[j]) rn+=l-1;
		if(a[l]*1.0<=md*b[j]) rn++;
		l=1;
		while(l<r){
			ll mid=(l+r+1)/2;
			(a[mid]*1.0<md*b[j])?l=mid:r=mid-1;
		}
		if(a[l]*1.0<md*b[j]) ln+=l;
	}
	return rn>=c;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(ll i=1;i<=n;i++) cin>>a[i];
	for(ll i=1;i<=n;i++) cin>>b[i];
	sort(a+1,a+n+1);
	sort(b+1,b+n+1);
	while(q--){
		cin>>c;
		double lc=0,rc=1e6;
		for(int kkk=1;kkk<=1000&&rc-lc>eps;kkk++){//注意实数二分的次数
			double mdc=(lc+rc)/2;
			(ck(mdc))?rc=mdc:lc=mdc;
		}
		ll asi=n,asj=1;
		double mn=1e6;
		for(ll j=(c*2<=n*n)?n:1;j<=n&&j;j+=(c*2<=n*n)?-1:1){//b_j
			ll la=1,ra=n;
			while(la<ra){
				ll mid=(la+ra)/2;
				(b[j]*lc<=a[mid])?ra=mid:la=mid+1;
			}
			if(jue(a[la]-b[j]*lc)<=mn) asi=la,asj=j,mn=jue(a[la]-b[j]*lc);
		}
		ll g=gcd(a[asi],b[asj]);
		cout<<a[asi]/g<<" "<<b[asj]/g<<"\n";
	}
	return 0;
}

T6[GDKOI2024 普及组] 刷野 I

题面描述:

Z 是一个与怪物战斗的巫师,这次他将面临 \(n\) 个站成一排的怪物,其中第 \(i\) 个怪物的生命值是 \(a_i\)

Z 率先使用一种攻击方式攻击,攻击过后所有血量小于等于 \(0\) 的怪物死亡。在 Z 攻击一次后,所有存活的怪物对 Z 造成 \(1\) 点伤害。以上步骤不断循环,直到 Z 击杀所有怪物为止。

Z 一共有三种攻击方式:

  • 普通攻击: 消耗 \(0\) 点能量值,选择一只怪物并使其血量减少一点。

  • 天音波: 消耗 \(1\) 点能量值,选择一只怪物并使其血量减少两点。

  • 天雷破: 消耗 \(1\) 点能量值,使所有怪物血量减少一点。

现在 Z 一共有 \(m\) 点能量,现在他想知道在最优的策略下,击败 \(n\) 只怪物所损失的最少血量。

思路:

记天音波为 \(c_1\),天雷破为 \(c_2\)
首先贪心易得,当不进行 \(c_1\)\(c_2\) 时,将 \(a_i\) 从小到大排序后进行攻击最优。所以此时他的答案为:

\[(\sum_{i=1}^{n}{a_i\times (n-i+1)})-n \]

因此,我们可以将对 \(a_i\) 使用一次 \(c_1\) 看成将 \(a_i\) 减去二,将使用一次 \(c_2\) 看成将在场所有的 \(a_i\) 都减去一。
最后,依照上面的式子可以得到如下的贪心策略:

  1. 有能量就要放大招。
  2. 对于一个怪物,要么只使用 \(c_1\),要么只使用 \(c_2\)(当 \(a_i\) 为奇数且只使用 \(c_1\) 时,将最后一次血量只剩 \(1\)\(c_1\) 要换为 \(c_2\))。

标签:

贪心

代码实现:

this
#include<bits/stdc++.h>
#define N 100005
#define ll long long
using namespace std;
ll n,m,a[N],ans;
int main(){ 
	cin>>n>>m;
	ans-=n;
	for(ll i=1;i<=n;i++) cin>>a[i];
	sort(a+1,a+n+1);
	ll c1=0,c2=0;
	for(ll i=1;i<=n;i++){
		if(a[i]<=c2) continue;
		ll t2=0,t1=n-i+1,ic1=0,ic2=c2,lst=0;
		while(a[i]>2*ic1+c2&&c1+c2<m){
			if(a[i]-ic1*2-ic2==1){
				c2++;
				break;
			}
			if(!lst){
				t2=0,t1=n-i+1;
				for(ll j=i+1;j<=n;j++) if(a[j]>c2) t2+=n-j+1;
				if(t2>=t1) c2++,lst=2;
				else c1++,ic1++,lst=1;
			}
			else{
				if(lst==2) c2++;
				else c1++,ic1++;
			}
		}
		a[i]-=ic2+ic1;
		ans+=a[i]*(n-i+1);
	}
	cout<<ans<<"\n";
	return 0;
}

T7(待完善)「HCOI-R1」孤独的 sxz

题面描述:

给你一个 \(n\times m\) 的网格和 \(k\) 个不重复的点 \((x_i,y_i)\) ,请你找出异于这 \(k\) 个点的一个点 \((a,b)\),使如下式子最大:

\[\sum_{i=1}^{k}{|a-x_i|+|b-y_i|} \]

思路:

\(x,y\) 分开排序。
将上述求和拆开得到:

\[\sum_{i=1}^{k}{|a-x_i|}+\sum_{i=1}^{k}{|b-y_i|} \]

发现形式相同,故暂时只对一个式子讨论。
设前 \(t_1\)\(x_i\) 都比 \(a\) 小且 \(t_1\) 后的 \(x_i\) 都比 \(a\) 大,则式子可以化为:

\[(2t_1-k)\times a-\sum_{i=1}^{t_1}{x_i}+\sum_{i=t1+1}^{k}{x_i} \]

可以发现,当 \(a\) 取得最大或者最小值时,该式取得最大值。
右边的式子同理。
所以,我们对这个矩阵的左上、左下、右上、右下这四个角角的四个边长为 \(1000\) 的正方形内进行遍历并依靠前缀和求出答案,并更新即可。

标签:

数学

代码实现:

this
#include<bits/stdc++.h>
#define ll long long
#define pr pair<ll,ll>
#define mr(a,b) make_pair(a,b)
#define K 400005
using namespace std;
ll n,m,k,ans,x[K],y[K];
map<pr,ll> vis;
ll solve(int i,int j){
	if(vis.count(mr(i,j))) return 0;
	ll l=1,r=k,ret=0;
	#define mid ((l+r+1)>>1)
	while(l<r) (x[mid]-x[mid-1]<=i)?(l=mid):(r=mid-1);
	if(l==1&&x[1]>i) l=0;
	ret+=(2*l-k)*i+x[k]-2*x[l];
	l=1,r=k;
	while(l<r) (y[mid]-y[mid-1]<=j)?(l=mid):(r=mid-1);
	if(l==1&&y[1]>j) l=0;
	return ret+(2*l-k)*j+y[k]-2*y[l];
}
int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++){
		cin>>x[i]>>y[i];
		vis[mr(x[i],y[i])]=1;
	}
	sort(x+1,x+k+1);
	sort(y+1,y+k+1);
	for(int i=1;i<=k;i++) x[i]+=x[i-1],y[i]+=y[i-1];
	int k1=min(1000ll,n),k2=min(1000ll,m);
	for(int i=1;i<=k1;i++){
		for(int j=1;j<=k2;j++){
			ans=max(ans,solve(i,j));
		}
		for(int j=m,nj=1;nj<=k2;j--,nj++){
			ans=max(ans,solve(i,j));
		}
	}
	for(int i=n,ni=1;ni<=k1;i--,ni++){
		for(int j=1;j<=k2;j++){
			ans=max(ans,solve(i,j));
		}
		for(int j=m,nj=1;nj<=k2;j--,nj++){
			ans=max(ans,solve(i,j));
		}
	}
	cout<<ans<<"\n";
	return 0;
}

注:

  1. 题号为本校OJ上的链接,题名为原出处链接。

$$The\ end$$

posted @ 2025-03-29 16:20  skx_515  阅读(22)  评论(0)    收藏  举报