Logo KSCD_ 的博客

博客

坦夫稼轩(互测赛)题解

...
KSCD_
2025-12-01 12:56:32
Defection,Retribution,Testify.

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-14 01:17:18

A.永遇乐

出题人谢罪:

本题原本的出题意图是考察分层图和虚点,但是发现两个都不需要……现在看来是 $k_1$ 给小了。

所以能跑过就行,大家能写出做法也很强,下面是参考做法,仅供参考

题意

求在无向图上从 $1$ 号节点出发,在 $k_1$ 个点中任意一个停留后到达 $k_2$ 个点中任意一个的最短路。

思路

考虑把在点上停留也转化成边,自然想到分层图。

建两层图,每层内连无向边,再把 $k_1$ 个点由第一层向第二层连权值为 $a_i$ 的有向边。答案即为以第二层 $k_2$ 个点为终点的最短路的最小值。

为了快速求出答案,可以将第二层的 $k_2$ 个点向虚点(代码中为 $0$ 号点)连边,这样最终答案即为 $1$ 到 $0$ 的最短路。

代码

#include<iostream>
#include<vector>
#include<queue>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+10;
const int maxs=1e15;
int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
    return s*w;
}
int n,m,ka,kb,d[N*2];
priority_queue <pii > q; 
bool f[N*2];\/\/因为建了两层图,所以要开两倍空间
struct edg
{
	int v,w;
};
vector <edg> edge[N*2];\/\/同上
void add(int u,int v,int w)
{
	edg t;
	t.w=w,t.v=v;
	edge[u].push_back(t);
}\/\/加边操作
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		add(u,v,w),add(v,u,w);
		add(n+u,n+v,w),add(n+v,n+u,w);\/\/层内建边
	}
	ka=read(),kb=read();
	for(int i=1;i<=ka;i++)
	{
		int t=read(),ta=read();
		add(t,n+t,ta);\/\/层间建边,即为停留的代价
	}
	for(int i=1;i<=kb;i++)
	{
		int t=read();
		add(n+t,0,0);\/\/连向虚点
	}
	for(int i=0;i<=2*n;i++) d[i]=maxs,f[i]=0;
	d[1]=0;\/\/初始化
	q.push({0,1});
	while(!q.empty())
	{
		int u=q.top().second; q.pop();
		if(f[u]) continue;
		f[u]=1;
		for(int i=0;i<edge[u].size();i++)
		{
			int ne=edge[u][i].v,tw=edge[u][i].w;
			if(d[ne]>d[u]+tw)
			{
				d[ne]=d[u]+tw;
				q.push({-d[ne],ne});
			}
		}
	}\/\/Dijkstra
	cout<<d[0];
	return 0;
}

B.贺新郎

题意

给出 $n$ 个数对 $(a_i,b_i)$,求一种排列使 $\sum_{i=1}^{n-1}\left| a_i-a_{i+1}\right|+\left| b_i-b_{i+1}\right|$ 最大。

思路

看数据范围显然是状压dp,这里可以把题意抽象成在平面直角坐标系中有 $n$ 个点,两点之间距离为曼哈顿距离,本题便转化成了从任意点开始到任意点结束的旅行商问题(可参照P1433),只不过本题要求最大值(当然这里不转化也是状压dp)。

代码

#include<iostream>
#include<cmath> 
#define int long long
using namespace std;
const int N=18+10;
const int M=3e5+10;
int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
    return s*w;
}
int n,ans=-1,x[N],y[N];
int s[N][N];\/\/存储跳跃度
int f[N][M];\/\/f[i][j]表示以i结尾,j状态可达到的最大效果值
int dis(int a,int b)
{
	return abs(x[a]-x[b])+abs(y[a]-y[b]);
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) x[i]=read();
	for(int i=1;i<=n;i++) y[i]=read();
	for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++)
		s[i][j]=dis(i,j),s[j][i]=s[i][j];
	for(int i=1;i<=n;i++) f[i][1<<(i-1)]=0;\/\/可从任意部分开始,但其实在这份代码里这个初始化没用
	for(int k=1;k<(1<<n);k++) for(int i=1;i<=n;i++)\/\/枚举状态和最终点
	{ 
		if(k&(1<<(i-1))==0)	continue;
		for(int j=1;j<=n;j++)\/\/枚举上一个点
		{
			if(i==j||(k&(1<<(j-1)))==0) continue;
			f[i][k]=max(f[i][k],f[j][k-(1<<(i-1))]+s[j][i]);\/\/转移
		}
	}
	for(int i=0;i<n;i++) ans=max(ans,f[i][(1<<n)-1]);\/\/可在任意部分结束,记录答案
	cout<<ans;
	return 0;
}

C.青玉案

没想到吧,这题是原题P4042,是出题人在qbxt做到的题) (虽然原题是紫,但个人觉得大概是蓝)

题意

每个烟花可以公开放,这样会得到若干个烟花并增加热闹值;也可以自己放,只会增加热闹值。求最小热闹值。

思路

若设 $f_i$ 为把第 $i$ 种烟花及其后产生的新烟花全部放完的最小代价,则显然有 $f_i=min(b_i,a_i+\sum_{j=1} ^{s_i}f_{v_j})$,其中 $v$ 是存储公开放这个烟花所产生的新烟花的数组。

显然仅当 $f_{v_j}$ 均小于 $f_i$ 时,$f_i$ 才可能从后边的式子转移。因此类似于最短路算法,把所有烟花放入堆中,按所需代价从小到大处理即可,具体看代码。

(其实好像还有一点拓扑的思想,即只有一种烟花会产生的所有烟花都处理完,才能处理该烟花对应式子的第二项)

代码

此处代码中的 $f_i$ 仅维护了上式的第二项,即 $a_i+\sum_{j=1} ^{s_i}f_{v_j}$。上式第一项由初始加入堆的元素维护,把答案存到了 ${ans}_i$ 中。

#include<iostream>
#include<vector>
#include<queue>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+10;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,a[N],b[N],s[N],f[N],ans[N];
bool v[N];
vector <int> edge[N];\/\/存储产生烟花情况
priority_queue <pii > q;
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read(),b[i]=read(),s[i]=read();
		for(int j=1;j<=s[i];j++)
		{
			int t=read();
			edge[t].push_back(i);\/\/由产生的烟花连向该烟花
		}
		q.push({-b[i],i});\/\/初始把每个烟花都以直接放完的代价放入队列
		f[i]=a[i];\/\/初值设为公开燃放的代价
	}
	while(!q.empty())
	{
		int u=q.top().second,w=-q.top().first; q.pop();
		if(v[u]) continue;
		v[u]=1,ans[u]=w;\/\/记录答案
		for(int i=0;i<edge[u].size();i++)
		{
			int ne=edge[u][i];
			if(v[ne]||f[ne]>b[ne]) continue;
			f[ne]+=w;\/\/此处就会将所有f_v_j加到f中
			s[ne]--;\/\/记录产生烟花中未处理的数量
			if(s[ne]==0) q.push({-f[ne],ne});\/\/若全部处理完则再加入堆
		}
	}\/\/Dijkstra思想
	cout<<ans[1];
	return 0;
}

D.破阵子

(类似于P1395,由于把树这个信息坑人地藏了起来,同时点有点权,评了绿)

题意

给定一棵树(因为每两个地区之间有且仅有一条路线,可以确定为一棵树,即保证 $m=n-1$),每将地区上的兵力移到直接连接的地区上时,就会消耗兵力数的代价,求集中所有兵力到同一地区的最小代价。

思路

既然说了是树那显然是树形dp了吧

典型换根dp,先钦定 $1$ 为根,设 ${cnt}_i$ 为子树中的兵力数,${sum}_i$ 为子树中所有兵力转移到子树根节点的代价,$j$ 为 $i$ 的子节点,则显然有 ${cnt}_i=a_i+\sum cnt_j$,${sum}_i=\sum (sum_j+cnt_j)$,一遍dfs即可处理完。

然后再计算以每个点为集中点的总代价 ${ans}_i$,其中 ${ans}_1={sum}_1$。若设总兵力数为 $tot$,$v$ 为原树中 $u$ 的子节点,则有:

${ans}_v={ans}_u-{cnt}_v+({tot}-{cnt}_v)={ans}_u+{tot}-2\times {cnt}_v$

这样再dfs一遍同时更新答案最小值即可。

代码

#include<iostream>
#include<vector>
#define int long long
using namespace std;
const int N=2e5+10;
const int maxs=1e15;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1;  ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,m,tot=0; 
vector <int> edge[N];
int a[N],sum[N],cnt[N],ans[N],minn=maxs;
void dfs(int u,int fat)
{
	sum[u]=0,cnt[u]=a[u];
	for(int i=0;i<edge[u].size();i++)
	{
		int ne=edge[u][i];
		if(ne==fat) continue;
		dfs(ne,u);
		cnt[u]+=cnt[ne],sum[u]+=(sum[ne]+cnt[ne]);
	}
}\/\/第一遍dfs记录cnt和sum
void dfsb(int u,int fat)
{
	minn=min(minn,ans[u]);
	for(int i=0;i<edge[u].size();i++)
	{
		int ne=edge[u][i];
		if(ne==fat) continue;
		ans[ne]=ans[u]+tot-cnt[ne]*2;
		dfsb(ne,u);
	}
}\/\/第二遍dfs记录答案
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read(),tot+=a[i];
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		edge[u].push_back(v),edge[v].push_back(u);
	}
	dfs(1,0);
	ans[1]=sum[1];
	dfsb(1,0);
	cout<<minn; 
	return 0;
}

E.水调歌头

(分组背包裸题,可参照P1757

(这题应该是最简单的吧,板子是橙,由于需要自己处理组别,所以我评了黄)

闲聊

这里不讲题意之类的了,只说下部分分,$20\%$ 的数据给的是不会滚动数组的(话说真的有人不会吗),$30\%$ 的数据显然每件事单独为一组,01背包即可,同时这个Subtask中由于还有 $y_i$ 和 $m_i$ 的大小限制,实际有 $n\leq816$ 。

(其实此题 $y_i$ 的限制是辛弃疾的生卒年)

std代码

#include<iostream>
using namespace std;
const int N=4e3+10;
const int K=4e5+10;
const int M=9e2+10;\/\/最多只有(1207-1140+1)*12=816个组
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,k,l=1e9,r=-1e9; \/\/这里记录的是下标最大和下标最小的组
int cnt[M],w[M][N],v[M][N],f[K];
int main()
{
	n=read(),k=read();
	for(int i=1;i<=n;i++)
	{
		int y=read(),m=read(),tw=read(),tv=read();
		int gr=(y-1140)*12+m;\/\/这里转化一下标号,方便存储
        cnt[gr]++,l=min(l,gr),r=max(r,gr);\/\/更新组数的范围
		w[gr][cnt[gr]]=tw,v[gr][cnt[gr]]=tv;\/\/分组存储
	}
	for(int i=l;i<=r;i++) for(int j=k;j>=0;j--)\/\/枚举每个组和最终的容量
	{
		bool tf=true;
		for(int tk=1;tk<=cnt[i];tk++) if(j-w[i][tk]>=0)\/\/枚举每件物品
				tf=false,f[j]=max(f[j],f[j-w[i][tk]]+v[i][tk]);
		if(tf) break;\/\/若所有物品都无法更新,则跳出循环(这里不剪枝会慢很多,但能过) 
	}
	cout<<f[k];
	return 0;
} 

评论

暂无评论

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。