做题记录

记录下做过的题,感觉一般的题就在这里记下,感觉不错的题就进好题选讲。

update:发现想写进好题选讲的题太少了,就只写做题记录了。

又 update:在标题上标注了好题。

P2198 杀蚂蚁

比较难崩的题目。

我们首先可以想到每个位置放上塔一定比不放要更优,其次可以想到把激光塔往后放一定是不劣的,因为在激光塔的伤害固定的前提下,让他走的越慢越好。那么我们就可以只考虑放置放射塔和干扰塔,设 \(dp_{i,j}\) 表示放置了 \(i\) 个放射塔和 \(j\) 个干扰塔的伤害最大值,由 \(dp_{i-1,j}\)\(dp_{i,j-1}\) 可以推得。

然后我们发现答案最大值甚至爆 long long,于是就只能用高精或者 int_128。

注意在状态设计的时候用类似背包的那种是不对的,因为如果前面时间增长了会影响后面吃毒,但是同时设出两个且递推,既满足从前往后排,又可以对于时间增长的后效性进行计算,所以应该同时递推两个塔。

P4933 大师

我们发现这个高度的值域很小,完全可以枚举,于是我们自然而然的想到从公差入手。设 \(dp_{i,j}\) 表示以第 \(i\) 位结尾且公差为 \(j\) 的等差数列的数量,注意到我们有些公差对答案没有贡献,则可选择直接枚举两个点,让点之间的高度做为公差求答案。则转移方程为 \(dp_{i,h_i-h_j} = dp_{j,h_i-h_j} + 1\)。时间复杂度 \(O(n^2)\)

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=998244353;
int dp[1005][80005],a[1005];



//function 
void solve(){
	
	
	
	return;
}
 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int ans=0,p=20000;
	for(int i=1;i<=n;i++){
		ans++;
		for(int j=1;j<i;j++){
			dp[i][a[i]-a[j]+p]+=dp[j][a[i]-a[j]+p]+1;
			dp[i][a[i]-a[j]+p]%=mod;
			ans+=dp[j][a[i]-a[j]+p]+1;
			ans%=mod;
		}
	}
	cout<<ans<<endl;
	
	
	return 0;
}

P10793 『SpOI - R1』Double Champions

死在读题上了。

我们发现贡献是 \(\min(r) - \max(l)\),左端点从小到大排序后直接扫一遍就行。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
struct node{
	int l,r;
}a[200005];	


//function 
bool cmp(node a,node b){
	if(a.l==b.l)return a.r>b.r;	
	else return a.l<b.l;
}
void solve(){
	int n,w;
	cin>>n>>w;
	for(int i=1;i<=n;i++)cin>>a[i].l>>a[i].r;
	if(w==0){
		cout<<1<<endl;
		return;
	}
	for(int i=1;i<=n;i++){
		if(a[i].r-a[i].l+1<w){
			cout<<"No"<<endl;
			return;
		}
	}
	sort(a+1,a+1+n,cmp);
	int ans=1,mi=inf;
	for(int i=1;i<=n;i++){
		mi=min(mi,a[i].r);
		if(mi-a[i].l+1<w){
			ans++;
			mi=a[i].r;
		}
	}
	cout<<ans<<endl;
	
	return;
}

 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int t;
	cin>>t;
	while(t--)solve();
	
	return 0;
}

P1768 天路(好题)

一道不错的图论复习题。

要求走过的路径形成一个环,那就想学过的有关环的图论算法:拓扑排序、SPFA、最小环、Tarjan。思考一下各自的特点:拓扑排序能判有没有环,SPFA 可以判负环,Tarjan 求出来的环是最大环。

看起来出路不是很多的样子,考虑回到题目。

题目中要求的是所有环中的 \(max(\sum v_i / \sum p_i)\),那么我们大概是可以想到找出所有简单环然后将环合并的一个东西,但随便想想就觉得这玩意不好做且时间复杂度大得要命。

从式子入手,令 \(max(\sum v_i / \sum p_i) = ans\),则满足对于所有的环存在 \(ans \ge \sum v_i / \sum p_i\),对式子稍做变形,得到 \(ans * \sum p_i - \sum v_i \ge 0\),即 \(\sum p_i*ans -\sum v_i \ge 0\)。我们发现在这个式子中,答案是具有单调性的。

于是我们就找到了一个很对的做法:二分答案,每次将边权设为 \(ans * p_i - v_i\),跑 SPFA 判负环。

顺嘴提一下,这题由于是实数二分,有两种方法,一种直接定义 \(eps\),另一种是循环二分次数,通常来说用第二种方法精度会更高些,但是要注意循环次数。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
const int N=7005;
int vis[N];
double dis[N];
vector<pair<int,double> >e[N];
vector<pair<int,pair<int,int> > >g[N];


//function 
void solve(){
	
	
	
	return;
}
bool spfa(int x){
	vis[x]=1;
	for(auto tmp:e[x]){
		int v=tmp.first;
		double w=tmp.second;
		if(dis[v]>dis[x]+w){
			if(vis[v])return false;
			else {
				dis[v]=dis[x]+w;
				if(!spfa(v))return false;
			}
		}
	}
	vis[x]=false;
	return true;
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,V,P;
		cin>>u>>v>>V>>P;
		g[u].push_back(mkp(v,mkp(V,P)));
	}
	//建超级原点
	for(int i=1;i<=n;i++)e[0].push_back(mkp(i,0));
	double l=0,r=200;
	for(int _=1;_<=15;_++){
		double mid=(l+r)/2;
		//建图
		for(int i=1;i<=n;i++)e[i].clear();
		for(int i=1;i<=n;i++){
			for(auto j:g[i]){
				int v=j.second.first,p=j.second.second;
				e[i].push_back(mkp(j.first,mid*p-v));
			}
		}
		for(int i=0;i<=n;i++)vis[i]=0;
		for(int i=1;i<=n;i++)dis[i]=inf;
		if(spfa(0))r=mid;
		else l=mid;
//		cout<<mid<<endl;
	}
	if(l==0)cout<<-1<<endl;
	else cout<<fixed<<setprecision(1)<<l<<endl;
	
	
	return 0;
}

P10884 [COCI 2017/2018 #2] San

小清新搜索 + 二位偏序。

考虑到部分分给了一个爆搜且对于正解依然不大的范围,我们很容易想到折半搜索。

我们设搜出来左半边的高度为 \(h_i\),金币数为 \(g_i\);右半边的高度为 \(h_j\),金币数为 \(g_j\)。对于可以合并的答案,我们显然有式子 \(h_i \le h_j\)\(k \ge g_i + g_j\),即 \(k - g_i \ge g_j\),这两个式子很显然可以在离散化后用二位偏序统计答案。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
const int N=3e6+5;
ll n,k,h[N],g[N];
ll tot,c[N],t[N];
struct node{
	ll h,sum,ty;
}s[N];


//function 
void solve(){
	
	
	
	return;
}
void dfs1(ll x,ll hi,ll sum){
	if(x>n/2){
		s[++tot].h=hi;
		s[tot].sum=max(k-sum,0*1ll);
		s[tot].ty=1;
		return;
	}
	if(h[x]>=hi)dfs1(x+1,h[x],sum+g[x]);
	dfs1(x+1,hi,sum);
}
void dfs2(ll x,ll hi,ll sum){
	if(x<=n/2){
		s[++tot].h=hi;
		s[tot].sum=sum;
		s[tot].ty=2;
		return;
	}
	if(h[x]<=hi)dfs2(x-1,h[x],sum+g[x]);
	dfs2(x-1,hi,sum);
}
bool cmp1(node a,node b){
	return a.sum<b.sum;
}
bool cmp2(node a,node b){
	if(a.h==b.h)return a.ty<b.ty;
	return a.h<b.h;
}
ll lowbit(ll x){
	return x & (-x);
}
void update(ll x,ll y){
	for(int i=x;i<=N-2;i+=lowbit(i))t[i]+=y;
	return;
}
ll query(ll x){
	ll res=0;
	for(int i=x;i>=1;i-=lowbit(i))res+=t[i];
	return res;
}

 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>h[i]>>g[i];
	
	dfs1(1,0,0);
	dfs2(n,inf,0);
	
	sort(s+1,s+1+tot,cmp1);
	for(int i=1;i<=tot;i++){
		if(s[i].sum==s[i-1].sum)c[i]=c[i-1];
		else c[i]=c[i-1]+1;
	}
	for(int i=1;i<=tot;i++)s[i].sum=c[i]+1;
	
	sort(s+1,s+1+tot,cmp2);
	ll ans=0;
	for(int i=1;i<=tot;i++){
		if(s[i].ty==1)update(s[i].sum,1);
		else ans+=query(s[i].sum);
	}
	cout<<ans<<endl;
	
	
	return 0;
}

P4665 [BalticOI 2015] Network

小清新图论。

我们可以将题意理解为要让每一条边都出现在至少一个环里,在这种情况下我们把叶子节点连起来一定是很优的。那我们可以选择找出来所有叶子节点把他们连起来。注意一下连法,我们可以发现连接 \((i,i+leaf/2)\) 总是正确的。证明可以对于某条边中儿子的叶子节点数分类讨论,或参考题解。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
vector<int>g[500005],ans;


//function 
void solve(){
	
	
	
	return;
}
int dfs(int u,int fa){
	int res=0;
	for(auto i:g[u]){
		if(i==fa)continue;
		res+=dfs(i,u);
	}
	if(res!=0)return res;
	else {
		ans.push_back(u);
		return 1;
	}
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n;
	cin>>n;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	if(g[1].size()==1)ans.push_back(1);
	int tmp=dfs(1,0);
	if(g[1].size()==1)tmp++;
	cout<<ceil(tmp/2.0)<<endl;
	for(int i=0;i<tmp/2;i++)cout<<ans[i]<<' '<<ans[i+tmp/2]<<endl;
	if(tmp%2==1)cout<<ans[0]<<' '<<ans[tmp-1]<<endl;
	
	
	return 0;
}


P4280 [AHOI2008] 逆序对

假设没有 \(-1\),那么我们对于原题直接求逆序对即可。

让我们考虑当存在 \(-1\) 时的情况。容易发现我们填入的数一定是单调不降的,容易证明这一结论。那么我们在枚举到 \(-1\) 时只需要考虑它和给定的数所形成的逆序对,枚举该位可以取到的数,同时考虑该数与左边和右边的数构成的逆序对,贪心的选择更小的数。开两个树状数组统计答案即可。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
const int N=1e4+5;
const int K=1e2+5;
int a[N],t1[K],t2[K];


//function 
void solve(){
	
	
	
	return;
}
int lowbit(int x){
	return x & (-x);
}
void update1(int x,int k){
	for(int i=x;i<=102;i+=lowbit(i))t1[i]+=k;
}
void update2(int x,int k){
	for(int i=x;i<=102;i+=lowbit(i))t2[i]+=k;
}
int query1(int x){
	int res=0;
	for(int i=x;i>0;i-=lowbit(i))res+=t1[i];
	return res;
}
int query2(int x){
	int res=0;
	for(int i=x;i>0;i-=lowbit(i))res+=t2[i];
	return res;
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++){
		if(a[i]!=-1)update2(a[i],1);
	}
	int l=1,sum=0,ans=0;
	for(int i=1;i<=n;i++){
		if(a[i]!=-1){
			ans+=i-sum-query1(a[i])-1;
			update1(a[i],1);
			update2(a[i],-1);
		}
		else {
			sum++;
			int res=inf,tmp=0;
			for(int j=l;j<=100;j++){
				int tmp1=i-sum-query1(j),tmp2=query2(j-1);
				if(tmp1+tmp2<res){
					res=tmp1+tmp2;
					tmp=j;
				}	
			}
			ans+=res;
			l=tmp;
		}
//		cout<<ans<<' ';
	}
//	cout<<endl;
	cout<<ans<<endl;
	
	
	return 0;
}


P4945 最后的战役

这个题有 DP 和贪心两种思路。

我们思考 DP,状态好设递推式好推,就变成了中位或下位绿题,时间复杂度 \(O(nm)\)

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
const int N=1e5+5;
int k[N],p[N],x[N];
map<int,int>mp;
int dp[50005][505];


//function 
void solve(){
	
	
	
	return;
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>k[i]>>p[i];
	for(int i=1;i<=n;i++)cin>>x[i];
	
	int ma=0;
	mp[k[1]]+=p[1];
	ma=max(ma,p[1]);
	dp[1][0]=max(mp[x[1]],ma);
	for(int i=2;i<=n;i++){
		mp[k[i]]+=p[i];
		ma=max(ma,p[i]);
		dp[i][0]=dp[i-1][0]+max(mp[x[i]],ma);
		for(int j=1;j<=min(i,m);j++){
			dp[i][j]=max(dp[i-2][j-1]+max(mp[x[i]],ma)*2,dp[i-1][j]+max(mp[x[i]],ma));
		}
	}
	/*
	int ans=0;
	for(int i=1;i<=m;i++)ans=max(ans,dp[n][i]);
	cout<<ans<<endl;
	*/
	cout<<dp[n][m]<<endl;
	
	
	return 0;
}

但如果我们强化一下这道题呢?

假设我们没有加倍魔法,则我们得到的最后答案是一个定值。设第 \(i\) 时,黑魔王获得的能量时 \(e_i\),则我们如果在第 \(i\) 时使用加倍魔法,则对答案的贡献是 \(e_{i+1} - e_i\),我们发现这还是可以确定的。于是我们发现这道题就变成了有 \(n\) 个数,不能选相邻数,最多选 \(m\) 个的最大获利是多少,该问题贪心可求。时间复杂度 \(O( (n + m) \log n)\)

P1484 种树(好题)

这题其实就是上一题的贪心方法的具体实现。

我们考虑贪心的选取更大的数,并删除掉其左右的数,用优先队列实现。但注意到有些时候这个思路在两边加起来大于中间时会假掉,于是我们考虑给他一次反悔的机会,不选择中间的而选择两边的。注意到我们由于已经选过中间的了,则我们重新再选两边的贡献为 \(num_{i-1}+num_{i+1}-num_i\),且仅多使用一次获取贡献的机会。于是我们可以降 \(i-1\)\(i+1\) 删掉,将 \(num_i\) 更新为 \(num_{i-1}+num_{i+1}-num_i\),然后将该点再次加入优先队列。

正确性证明其实是好证的,如果我们已经选择过 \(num_i\),则若不选择两边而选择 \(num_{i+2}\) 更优的话,则优先队列会优先弹出 \(num_{i+2}\) 而并非更新后的 \(num_i\)

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
const int N=3e5+5;
struct node{
	int id,w;
	bool operator < (node a) const {
		return w<a.w;
	}
}a[N];
int nxt[N],lst[N],vis[N];


//function 
void solve(){
	
	
	
	return;
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>a[i].w;
	for(int i=1;i<=n;i++)a[i].id=i;
	for(int i=1;i<=n;i++)nxt[i]=i+1;
	for(int i=1;i<=n;i++)lst[i]=i-1;
	
	priority_queue<node>pq;
	for(int i=1;i<=n;i++)pq.push(a[i]);
	ll ans=0;
	for(int i=1;i<=k;i++){
		while(vis[pq.top().id] && !pq.empty())pq.pop();
		node tmp=pq.top();
		int id=tmp.id,w=tmp.w;
		pq.pop();
		if(w<=0)break;
		ans+=w;
		vis[nxt[id]]=vis[lst[id]]=1;
		//链表修改
		a[id].w=a[nxt[id]].w+a[lst[id]].w-a[id].w;
		nxt[id]=nxt[nxt[id]];
		lst[id]=lst[lst[id]];
		nxt[lst[id]]=id;
		lst[nxt[id]]=id;
		pq.push(a[id]);
	}
	cout<<ans<<endl;
	
	
	
	return 0;
}


posted @ 2025-08-19 01:30  -Delete  阅读(2)  评论(0)    收藏  举报