【做题记录】csp2025-贪心专题

A. [NOIP2015 普及组] 推销员
首先考虑一个明显假的贪心,选择前 \(X\) 大的疲劳值计算答案。
它假就假在,可以选择一个(或几个)疲劳值更小,但更远的位置,使总贡献更大。
略经思考后发现,如果要更换,那么一定要满足距离比当前的所有都远,而且更换掉的一定是当前最小的疲劳值。
同时,如果更换 \(2\) 个,则新的这 \(2\) 个疲劳值一定会都比当前的小,而距离却只会按更远的那个计算,因此一定不如更换一个更优。
故只需从前 \(X\) 大和更换掉一个的两种答案中取 \(\max\) 即可。具体实现可以用前缀和、前后缀最小值。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,sum[maxn],f[maxn],g[maxn];
struct node{
	int a,b;
}p[maxn];
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n);
	for(int i=1;i<=n;i++){
		read(p[i].a);
	}
	for(int i=1;i<=n;i++){
		read(p[i].b);
	}
	sort(p+1,p+n+1,[](const node &x,const node &y){return x.b>y.b;});
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+p[i].b;
		f[i]=max(f[i-1],p[i].a<<1);
	}
	for(int i=n;i;i--){
		g[i]=max(g[i+1],(p[i].a<<1)+p[i].b);
	}
	for(int i=1;i<=n;i++){
		printf("%d\n",max(sum[i]+f[i],sum[i-1]+g[i]));
	}
	return 0;
}
}
int main(){return asbt::main();}

B. Two Heaps
考虑如果所有数都不一样,那么答案就为 \(n^2\)
如果有一个数出现了两次,那么显然一个放这边另一个放那边更优。
如果出现次数大于 \(2\),那么剩下的是做不了贡献的,乱放。
因此步骤为:
\(1.\) 将所有出现次数 \(\ge 2\) 的在两边各放一个,剩下的留着。
\(2.\) 将所有出现次数为 \(1\) 的平均分配给两边。
\(3.\) 用第 \(1\) 步中剩下的数填满两个集合。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=205;
int n,ans[maxn],num[maxn];
struct node{
	int zhi,hao;
}a[maxn];
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n);
	for(int i=1;i<=n<<1;i++){
		read(a[i].zhi);
		num[a[i].zhi]++;
		a[i].hao=i;
	}
	sort(a+1,a+(n<<1|1),[](const node &x,const node &y){return x.zhi<y.zhi;});
	int cnt1=0,cnt2=0;
	for(int i=10;i<=99;i++){
		if(num[i]>1){
			cnt1++,cnt2++;
		}
	}
	for(int i=10;i<=99;i++){
		if(num[i]==1){
			if(cnt1<=cnt2){
				cnt1++;
			}
			else{
				cnt2++;
			}
		}
	}
	printf("%d\n",cnt1*cnt2);
	cnt1=cnt2=0;
	for(int i=1;i<=n<<1;i++){
		if(num[a[i].zhi]>1){
			ans[a[i].hao]=1;
			ans[a[++i].hao]=2;
			cnt1++,cnt2++;
			while(a[i+1].zhi==a[i].zhi){
				i++;
			}
		}
	}
	for(int i=1;i<=n<<1;i++){
		if(num[a[i].zhi]==1){
			if(cnt1<=cnt2){
				cnt1++;
				ans[a[i].hao]=1;
			}
			else{
				cnt2++;
				ans[a[i].hao]=2;
			}
		}
	}
	for(int i=1;i<=n<<1;i++){
		if(num[a[i].zhi]>2){
			i++;
			while(a[i+1].zhi==a[i].zhi){
				i++;
				if(cnt1<=cnt2){
					cnt1++;
					ans[a[i].hao]=1;
				}
				else{
					cnt2++;
					ans[a[i].hao]=2;
				}
			}
		}
	}
//	cout<<cnt1<<" "<<cnt2<<"\n";
	for(int i=1;i<=n<<1;i++){
		printf("%d ",ans[i]);
	}
	return 0;
}
}
int main(){return asbt::main();}

C. Antichain
首先,题目给出的图一定是一堆链构成的,其中有的点只在一条链中,有的同时在两条链中。

首先,一条链中一定只能选一个点,即答案最大为链的数量。然后考虑这么个事,如果选择了只在一条链中的点,那么会废掉一条链;然而如果选择了在两条链中的点,那就会废掉两条链。因此贪心策略为首先选择只在一条链中的点,然后再考虑在两条链中的点。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e6+5;
int n;
char s[maxn];
bitset<maxn> vis;
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	scanf(" %s",s);
	n=strlen(s);
	int ans=0;
	for(int u=0,v;u<n;u++){
		if(vis[u]){
			continue;
		}
		if(s[u]==s[(u-1+n)%n]){
//			cout<<u<<"\n";
			ans++,vis[u]=1,v=u;
			while(s[v]==s[u]){
				v=(v+1)%n;
				vis[v]=1;
			}
			v=u;
			while(s[(v-1+n)%n]==s[u]){
				v=(v-1+n)%n;
				vis[v]=1;
			}
		}
	}
	for(int u=0;u<n;u++){
		if(vis[u]){
			continue;
		}
//		cout<<u<<"\n";
		ans++;
		vis[u]=vis[(u+1)%n]=vis[(u-1+n)%n]=1;
	}
	printf("%d",ans);
	return 0;
}
}
int main(){return asbt::main();}

D. [AGC057A] Antichain of Integer Strings
\(f(x)\) 为最小的大于 \(x\)\(y\),使得 \(x\)\(y\) 的子串。易得:

\[f(x)=\min(10x,x+10^{|x|}) \]

其中 \(|x|\) 表示 \(x\) 的位数。
可以发现,\(f(x)\) 为一个严格单调递增的函数。
考虑贪心策略,显然选小的数不如选大的数优,因为小的数更有可能成为别的数的子串。于是,我们要求的其实就是这样一个集合 \(\mathbb{A}\),满足:

\[\mathbb{A}=\{x\in[l,r]\mid f(x)>r\} \]

因为 \(f(x)\) 是严格单增的,因此二分即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define int ll
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int pw10[]={
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
10000000000,
100000000000};
il int len(int x){
	int res=0;
	do{
		res++,x/=10;
	}while(x);
	return res;
}
il int f(int x){
	return min(x*10,x+pw10[len(x)]);
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
signed main(){
//	for(int i=1;i<=33;i++){
//		cout<<i<<" "<<f(i)<<"\n";
//	}
	int T;
	read(T);
	while(T--){
		int l,r,L,R;
		read(l)read(r);
		L=l,R=r;
		while(l<r){
			int mid=(l+r)>>1;
			if(f(mid)>R){
				r=mid;
			}
			else{
				l=mid+1;
			}
		}
		printf("%d\n",R-l+1);
	}
	return 0;
}
}
signed main(){return asbt::main();}

E. [USACO10MAR] Barn Allocation G
先按左端点升序排序,再按右端点升序排序。然后跑一个线段树区间减一,区间取 \(\min\)。这样的做法拿了 \(57 pts\)。然后再反着跑一遍,就过了。
原因是,正解做法为按右端点升序排序然后正着跑,我是按左端点升序排序再反着跑,那肯定是一样的。。。
正确性证明,因为按右端点升序排序,所以新加的右端点大于后加的。如果有重合,选新的而不选旧的会导致白白给多出来的这一块减了一个 \(1\),显然不如不选优。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define lid id<<1
#define rid id<<1|1
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,a[maxn];
struct node{
	int l,r;
}p[maxn];
struct stree{
	int zhi[maxn<<2],tag[maxn<<2];
	il void pushup(int id){
		zhi[id]=min(zhi[lid],zhi[rid]);
	}
	il void pushdown(int id){
		if(tag[id]){
			tag[lid]+=tag[id],tag[rid]+=tag[id];
			zhi[lid]+=tag[id],zhi[rid]+=tag[id];
			tag[id]=0;
		}
	}
	il void build(int id,int l,int r){
		tag[id]=0;
		if(l==r){
			zhi[id]=a[l];
			return ;
		}
		int mid=(l+r)>>1;
		build(lid,l,mid);
		build(rid,mid+1,r);
		pushup(id);
	}
	il void upd(int id,int L,int R,int l,int r,int val){
		if(L>=l&&R<=r){
			tag[id]+=val,zhi[id]+=val;
			return ;
		}
		pushdown(id);
		int mid=(L+R)>>1;
		if(l<=mid){
			upd(lid,L,mid,l,r,val);
		}
		if(r>mid){
			upd(rid,mid+1,R,l,r,val);
		}
		pushup(id);
	}
	il int query(int id,int L,int R,int l,int r){
		if(L>=l&&R<=r){
			return zhi[id];
		}
		pushdown(id);
		int mid=(L+R)>>1;
		if(r<=mid){
			return query(lid,L,mid,l,r);
		}
		if(l>mid){
			return query(rid,mid+1,R,l,r);
		}
		return min(query(lid,L,mid,l,r),query(rid,mid+1,R,l,r));
	}
}SG;
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	for(int i=1;i<=n;i++){
		read(a[i]);
	}
	for(int i=1;i<=m;i++){
		read(p[i].l)read(p[i].r);
	}
	sort(p+1,p+m+1,[](const node &x,const node &y){return x.l<y.l||x.l==y.l&&x.r<y.r;});
	SG.build(1,1,n);
	int ans1=0,ans2=0;
	for(int i=1;i<=m;i++){
		if(SG.query(1,1,n,p[i].l,p[i].r)){
			SG.upd(1,1,n,p[i].l,p[i].r,-1);
			ans1++;
//			cout<<i<<"\n";
		}
	}
	SG.build(1,1,n);
	for(int i=m;i;i--){
		if(SG.query(1,1,n,p[i].l,p[i].r)){
			SG.upd(1,1,n,p[i].l,p[i].r,-1);
			ans2++;
//			cout<<i<<"\n";
		}
	}
	printf("%d",max(ans1,ans2));
	return 0;
}
}
int main(){return asbt::main();}

F. [USACO09OCT] Allowance G
因为所有面值都可以整除比它大的面值,所以大的面值一定可以用小的凑出来,所以优先用大面值,剩下的用一个小面值来补就行了。
时间复杂度,每个硬币最多被用一次,这一部分是 \(O(\sum B)\);死循环中每次的硬币选择方式都是不同的,这一部分是 \(O(n2^n)\),都可以过。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pii pair<int,int>
#define mp make_pair
#define fir first
#define sec second
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
int n,m,ans;
vector<pii> q;
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	for(int i=1,a,b;i<=n;i++){
		read(a)read(b);
		if(a>=m){
			ans+=b;
		}
		else{
			q.pb(mp(a,b));
		}
	}
	sort(q.begin(),q.end());
	for(;;){
		int tmp=m;
		for(int i=q.size()-1;~i;i--){
			while(tmp>=q[i].fir&&q[i].sec){
				tmp-=q[i].fir,q[i].sec--;
			}
		}
		if(tmp>0){
			for(int i=0;i<q.size();i++){
				if(q[i].fir>=tmp&&q[i].sec){
					q[i].sec--,tmp=0;
					break;
				}
			}
		}
		if(tmp>0){
			break;
		}
		ans++;
	}
	printf("%d",ans);
	return 0;
}
}
int main(){return asbt::main();}

G. Competition
如果在一个 \(n\) 阶楼梯上有 \(\le n\) 位运动员,则一定是合法的。
考虑每个运动员对应着一个区间 \([l,r]\),表示这个人能到达的台阶。举个例子:

把台阶的顶端从左往右编号,则 \(1\) 对应 \([1,3]\)\(2\) 对应 \([2,3]\)\(3\) 对应 \([4,4]\)\(4\) 对应 \([3,5]\)
那么我们只需要选择一些区间,满足存在一些点使可以不重复地给每个区间一个包含的点。
这是一个贪心,将所有区间按左端点排序,从左向右扫 \(n\) 个位置,扫到 \(i\) 时加入左端点为 \(i\) 的区间,此时所有的区间都是包含了 \(i\) 的,贪心地选择右端点最小的,然后再删去右端点为 \(i\) 的区间。具体实现可以用堆。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m;
struct node{
	int id,l,r;
	il bool operator<(const node &x)const{
		return r>x.r;
	}
}p[maxn];
priority_queue<node> q;
vector<int> ans;
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	for(int i=1,x,y;i<=m;i++){
		read(x)read(y);
		p[i]=(node){i,n-y+1,x};
	}
	sort(p+1,p+m+1,[](const node &x,const node &y){return x.l<y.l;});
	for(int i=1,j=1;i<=n;i++){
		while(p[j].l==i){
			q.push(p[j++]);
		}
		if(q.size()){
			ans.pb(q.top().id);
			q.pop();
		}
		while(q.size()&&q.top().r==i){
			q.pop();
		}
	}
	printf("%d\n",ans.size());
	for(int x:ans){
		printf("%d ",x);
	}
	return 0;
}
}
int main(){return asbt::main();}

H. Jeff and Permutation
首先将所有 \(a_i\) 都取绝对值,不影响答案。
考虑怎样会产生逆序对(\(i<j\)):

  • \(a_i<a_j\),则需要把 \(a_j\) 变成负的,\(a_i\) 变不变无所谓。
  • \(a_i>a_j\),则 \(a_i\) 不能变,\(a_j\) 变不变无所谓。

因此统计 \(a_i\) 前面比它小的、后面比它小的,即将 \(a_i\) 改为负数、不改为负数会增加的逆序对数,二者取 \(\min\) 即可。
在左侧不能取小于等于,因为如果 \(a_i\) 的左侧比它小的数都已经小于右侧了,那么它左边的一个相同的数肯定也是这样。换句话说对于同一个数,一定是前面一部分变成负的,后面一部分不变。所以相同的数之间是不会产生贡献的。
\(a_i\) 变不变号也不会影响其他位置的答案,因为较小的数变或不变号都是不会影响答案的。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e3+5;
int n,a[maxn];
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n);
	for(int i=1;i<=n;i++){
		read(a[i]);
		a[i]=abs(a[i]);
	}
	int ans=0;
	for(int i=1,cnt1,cnt2;i<=n;i++){
		cnt1=cnt2=0;
		for(int j=1;j<i;j++){
			if(a[j]<a[i]){
				cnt1++;
			}
		}
		for(int j=i+1;j<=n;j++){
			if(a[j]<a[i]){
				cnt2++;
			}
		}
		ans+=min(cnt1,cnt2);
	}
	printf("%d",ans);
	return 0;
}
}
int main(){return asbt::main();}

I. [HEOI2015] 兔子与樱花
贪心策略:从下往上,对于每个点不断选择代价最小的儿子删除。
正确性:首先,选择最小的代价来删显然是没问题的。考虑在节点 \(u\),若删除了它的一个儿子,那么可能会导致它的父亲本来可以删它,现在却删不了了。这样先多删一个再少删一个,是不会影响答案的。因此此策略正确。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e6+5;
int n,m,a[maxn],ans;
vector<int> e[maxn];
il void dfs(int u){
	for(int v:e[u]){
		dfs(v);
	}
	sort(e[u].begin(),e[u].end(),[](const int &x,const int &y){return a[x]<a[y];});
	for(int v:e[u]){
		if(a[u]+a[v]-1<=m){
			a[u]+=a[v]-1;
			ans++;
		}
		else{
			break;
		}
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	for(int i=0;i<n;i++){
		read(a[i]);
	}
	for(int i=0,tot;i<n;i++){
		read(tot);
		a[i]+=tot;
		for(int j=1,x;j<=tot;j++){
			read(x);
			e[i].pb(x);
		}
	}
	dfs(0);
	printf("%d",ans);
	return 0;
}
}
int main(){return asbt::main();}

J. 展翅翱翔之时 (はばたきのとき)
\(a_i\)\(i\) 连边,题目给出的就是一个外向基环树森林。题目的要求是将它变成一个环。考虑将每棵树分开考虑。
思路是先断成一条条链,然后再连起来。对于每个子树上的点,只保留代价最大的儿子,其他儿子全部删去。这样就变成了一个环,向外伸出一堆链的形态。
现在枚举环上的每个点,设为 \(u\),则此时 \(a_u\) 是连出了两条边的(连向 \(u\) 的和伸出链的),一定要删去一条。记录删去所有链的代价和将环断开并保留一条链的代价,进行转移即可。时间复杂度 \(O(n)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
const ll inf=0x3f3f3f3f3f3f3f3f;
int n,a[maxn],deg[maxn];
int cnt,idx[maxn];
ll b[maxn],ans,liu[maxn];
bitset<maxn> vis;
vector<int> e[maxn];
il void dfs1(int u){
	vis[u]=1;
	idx[++cnt]=u;
	for(int v:e[u]){
		if(vis[v]){
			continue;
		}
		dfs1(v);
	}
}
queue<int> q;
il void dfs2(int u){
	ll mx=0,sum=0;
	for(int v:e[u]){
		if(deg[v]){
			continue;
		}
		sum+=b[v],mx=max(mx,b[v]);
		dfs2(v);
	}
	ans+=sum-mx,liu[u]=mx;
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n);
	for(int i=1;i<=n;i++){
		read(a[i])read(b[i]);
		e[a[i]].pb(i);
		deg[a[i]]++;
	}
	for(int i=1;i<=n;i++){
		if(!deg[i]){
			q.push(i);
		}
	}
	while(q.size()){
		int u=q.front();
		q.pop();
		if(--deg[a[u]]==0){
			q.push(a[u]);
		}
	}
	for(int x=1,tot;x<=n;x++){
		if(vis[x]||!deg[x]){
			continue;
		}
//		cout<<x<<"\n";
		tot=cnt+1;
//		puts("666");
		dfs1(x);
		if(cnt-tot+1==n){
			for(int i=1;i<=n;i++){
				if(!deg[i]){
					goto togo;
				}
			}
			puts("0");
			return 0;
			togo:;
		}
		for(int i=tot;i<=cnt;i++){
			if(deg[idx[i]]){
				dfs2(idx[i]);
			}
		}
//		cout<<ans<<"\n";
		ll sum=0,res=inf;
		for(int i=tot;i<=cnt;i++){
			if(deg[idx[i]]){
				res=min(min(sum,res)+b[idx[i]],res+liu[a[idx[i]]]);
				sum+=liu[a[idx[i]]];
			}
		}
		ans+=res;
	}
	printf("%lld",ans);
	return 0;
}
}
int main(){return asbt::main();}
/*
10
3 12
5 6
1 16
5 3
7 3
10 11
4 20
10 12
7 7
10 3
32
*/
posted @ 2024-12-30 14:23  zhangxy__hp  阅读(67)  评论(0)    收藏  举报