牛客周赛 Round 32(A,B,C,D,E,F)

比赛链接

官方视频讲解

这场D是用dfs跑图的一个树上dp,E是裸状压,F是状压DP,会状压的话其实难度不是特别大。B题出乎意料的卡了我一会,实际上如果推理出来一个小性质写起来就很简单了。


A 小红的 01 背包

思路:

V的容量能装多少个x就装多少个,然后个数乘以收益y就行了

code:

cpp 复制代码
#include <iostream>
#include <cstdio>
using namespace std;

int V,x,y;

int main(){
	cin>>V>>x>>y;
	cout<<V/x*y;
	return 0;
}

B 小红的 dfs

思路:

可以枚举哪一行哪一列是dfs,看一下其他地方是不是没有dfs,算一下变成这样需要多少次修改,暴力枚举一遍然后记录最小修改次数。

也可以跑DFS,每一位枚举修改成d,f,s以及保留原字符的情况,然后最后判断一下最终局面是否合理,然后记录最小修改次数,这是 O ( 4 9 ) O(4^9) O(49) 的。也不是不行,也点题了。

有一个小小的性质,只有可能第一行与列为dfs,或者第二行与列为dfs,或者第三行与列为dfs

其实很好证明,如果第一行为dfs,那么只有可能第一列出现dfs,第二列开头是f直接死了,第三列同理;同理如果第二行为dfs,那么只有可能第二列出现dfs,第一列第二个字符是d直接死了,第三列同理;同样的可以推出如果第三行为dfs,那么只有可能第三列出现dfs。反过来第一二三列是dfs也是如此。

所以根本不需要上面的讨论,只需要看第一行与列修改为dfs,第二行与列修改为dfs,第三行与列修改为dfs,三种情况哪个次数少就行了。

code:

暴力dfs version:

cpp 复制代码
#include <iostream>
#include <cstdio>
using namespace std;

char s[15];

bool check(){
	bool f=false;
	if(s[1]=='d' && s[2]=='f' && s[3]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(s[4]=='d' && s[5]=='f' && s[6]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(s[7]=='d' && s[8]=='f' && s[9]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(!f)return false;
	f=false;
	if(s[1]=='d' && s[4]=='f' && s[7]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(s[2]=='d' && s[5]=='f' && s[8]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(s[3]=='d' && s[6]=='f' && s[9]=='s'){
		if(!f)f=true;
		else return false;
	}
	if(!f)return false;
	return true;
}
int ans=9;
void dfs(int i,int cnt){
	if(i>9){
		if(check()){
			ans=min(cnt,ans);
		}
		return;
	}
	char t=s[i];
	dfs(i+1,cnt);
	if(t!='d'){
		s[i]='d';
		dfs(i+1,cnt+1);
		s[i]=t;
	}
	if(t!='f'){
		s[i]='f';
		dfs(i+1,cnt+1);
		s[i]=t;
	}
	if(t!='s'){
		s[i]='s';
		dfs(i+1,cnt+1);
		s[i]=t;
	}
}

int main(){
	for(int i=1;i<=9;i++)
		cin>>s[i];
	dfs(1,0);
	cout<<ans<<endl;
	return 0;
}

优雅结论 version:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

string t,s[3];

int main(){
	cin>>s[0]>>s[1]>>s[2];
	t="dfs";
	int ans=5;
	for(int i=0;i<3;i++){
		int res=0;
		for(int j=0;j<3;j++){
			if(s[i][j]!=t[j])res++;
			if(i!=j && s[j][i]!=t[j])res++;
		}
		ans=min(ans,res);
	}
	cout<<ans;
	return 0;
}

C 小红的排列生成

思路:

因为是把原数组修改为一个排列,所以不妨假设修改后就是1 2 3...。现在就是找某个元素放在哪个位置上,然后把这个元素修改成这个位置的值。

容易想到小的肯定放在小的位置上,大的放在大的位置上,不过因为没有遗漏都要放入,所以朴素的想法就是对原数组直接排序,然后按位置放就行了。但是这样为什么是对的呢。

如果 a i a_i ai 不放在 位置 i i i 而放在位置 i + 1 i+1 i+1 上时, a i + 1 a_{i+1} ai+1 就要放在位置 i i i 上, a i a_i ai 的贡献增大了,要想让总贡献更低,需要 a i + 1 a_{i+1} ai+1 就要放在位置 i i i 上会减少贡献。从绝对值的含义(两点间的距离),就是 a i + 1 a_{i+1} ai+1 离 i i i 更近,那就说明 a i + 1 ≤ i a_{i+1}\le i ai+1≤i,因此 a i ≤ a i + 1 ≤ i a_{i}\le a_{i+1}\le i ai≤ai+1≤i,总的答案其实没有变化,看图可以,推式子也行(原本是 ∣ a i − i ∣ + ∣ a i + 1 − i − 1 ∣ = i − a i + i + 1 − a i + 1 = 2 ∗ i + 1 − a i − a i + 1 |a_i-i|+|a_{i+1}-i-1|=i-a_i+i+1-a_{i+1}=2*i+1-a_{i}-a_{i+1} ∣ai−i∣+∣ai+1−i−1∣=i−ai+i+1−ai+1=2∗i+1−ai−ai+1,后来是 ∣ a i − i − 1 ∣ + ∣ a i + 1 − i ∣ = i + 1 − a i + i − a i + 1 = 2 ∗ i + 1 − a i − a i + 1 |a_i-i-1|+|a_{i+1}-i|=i+1-a_i+i-a_{i+1}=2*i+1-a_{i}-a_{i+1} ∣ai−i−1∣+∣ai+1−i∣=i+1−ai+i−ai+1=2∗i+1−ai−ai+1,没有变化)。你让 a i a_i ai 变成 i + 1 i+1 i+1 而 a i + 1 a_{i+1} ai+1 变成 i i i 和原本相比其实并没有让答案变得更优。类推, a i a_i ai 和 a j a_j aj ( i < j ) (i<j) (i<j) 交换位置也不会让答案变得更优,因此上面的想法是对的。

code:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;
typedef long long ll;

int n,a[maxn];

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	sort(a+1,a+n+1);
	ll ans=0;
	for(int i=1;i<=n;i++){
		ans+=abs(a[i]-i);
	}
	cout<<ans;
}

D 小红的二进制树

思路:

还是比较裸的树上DP,不过因为需要从根节点向下走,给的还是个图,所以需要建图,然后从根节点dfs。边dfs边跑树上dp。

设 d p [ i ] dp[i] dp[i] 表示以 i i i 节点为起点,向子树方向走可以得到的奇数个数,奇数的二进制最后一位就是1。所以把儿子节点的dp值加起来,再把儿子节点为 1 的个数加入答案,就是 d p [ i ] dp[i] dp[i] 的值。

code:

cpp 复制代码
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e5+5;

int n;
char ch[maxn];
int dp[maxn];

int head[maxn],cnt;
struct edge{
	int v,nxt;
}e[maxn<<1];
void add(int u,int v){
	e[++cnt].v=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}

void dfs(int u,int fa){
	for(int i=head[u],v;i;i=e[i].nxt){
		v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		dp[u]+=dp[v]+(ch[v]=='1');
	}
}

int main(){
	cin>>n>>ch+1;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs(1,-1);
	for(int i=1;i<=n;i++)
		cout<<dp[i]<<endl;
	return 0;
}

E 小红的回文数

思路:

截取一段连续区间,统计每种数字的个数,最多出现一种数字的数量为奇数的区间就是一个可行解。

其实我们只关心一个区间内每种数字的奇偶性,而不关系具体有几个,而且这个奇偶性可以通过前缀和来维护,比如数字1在区间 1 ∼ l − 1 1\sim l-1 1∼l−1 出现了奇数次,在区间 1 ∼ r 1\sim r 1∼r 出现了偶数次,那么知道区间 l ∼ r l\sim r l∼r 中出现了奇数次,而这个奇偶关系可以二进制状压,两个前缀和可以通过按位异或来得到区间某个数字数量的奇偶性。

十个数字就只需要十个二进制位,这样就可以使用一个数来表示前缀 1 ∼ i 1\sim i 1∼i 中的每个数字的奇偶性。如果我们记录一下前面右端点分别为 0 ∼ i − 1 0\sim i-1 0∼i−1 的前缀和,就可以一个一个枚举一下然后看一下左端点为 1 ∼ i 1\sim i 1∼i,右端点为 i i i 的区间是不是最多只有一种数字的数量为奇数。

不过一个一个枚举肯定会超时,考虑如何优化。其实右端点为 i i i 的前缀和跑出来之后,能够产生贡献的状态就已经确定了,只有异或后所有数位都为0,以及异或后有一个数位为1的情况可以产生贡献,我们只需要统计 0 ∼ i − 1 0\sim i-1 0∼i−1 的前缀和中有多少个上面的11种情况的状态就行了。因为10位二进制数总共就1024种不同的贡献,开桶统计每一种状态出现了几次即可。

long年开long long

code:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int a[1030],s;
long long ans;
string str;

int main(){
	cin>>str;
	a[0]++;
	for(auto ch:str){
		int t=ch^48;
		s^=(1<<t);
		
		ans+=a[s];
		for(int i=0;i<=9;i++)
			ans+=a[s^(1<<i)];
		a[s]++;
	}
	cout<<ans<<endl;
	return 0;
}

F 小红的矩阵修改

思路:

n只有4,蹊跷,非常蹊跷哇。

考虑如果把每一列看作一个整体,或者说一个状态,之后推第 i i i 列的时候,只需要保证第 i i i 列状态本身不冲突,以及和前面第 i − 1 i-1 i−1 列不冲突即可推过来,推过来的代价就是把第 i i i 列原本的状态修改为修改后的状态的次数加前第 i − 1 i-1 i−1 列的修改次数。一直推到第 m m m 列。

考虑跑dp。把一列看作一个状态,使用状压,可以把r看作0,e看作1,d看作2,这样就是一个n位的三进制数。设 d p [ s t ] [ i ] dp[st][i] dp[st][i] 表示第 i i i 列修改为状态st的最少修改次数。枚举第 i i i 列的状态 s t st st ,检查一下状态 s t st st 是否合法,如果合法,记录一下 c n t = m o d ( i , s t ) cnt=mod(i,st) cnt=mod(i,st) 为把第i列修改为st状态的修改次数。然后枚举第 i − 1 i-1 i−1 列的状态 s t 2 st2 st2,如果 s t st st 和 s t 2 st2 st2 不冲突,尝试从 d p [ s t 2 ] [ i − 1 ] dp[st2][i-1] dp[st2][i−1] 推到 d p [ s t ] [ i ] + c n t dp[st][i]+cnt dp[st][i]+cnt 即可。

最后枚举 s t st st,记录最小的 d p [ s t ] [ m ] dp[st][m] dp[st][m]

code:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
using namespace std;
const int maxm=1005;

int n,m,maxst;
string s[5];
int dp[100][maxm];//dp[st][i] 第i个位置为状态st的最小修改次数 

map<char,int> mp; 

int g(int i,int st){//取出三进制数第i位的数 
	for(int j=0;j<i;j++)st/=3;
	return st%3;
}
int mod(int col,int st){//第col列修改为st状态的次数 
	int cnt=0;
	for(int i=0;i<n;i++)
		if(mp[s[i][col-1]]!=g(i,st))
			cnt++;
	return cnt;
}
bool check(int st){//检查st有无相邻重复部分 
	for(int i=1;i<n;i++)
		if(g(i-1,st)==g(i,st))
			return false;
	return true;
}
bool check(int st1,int st2){//检查st1和st2有无重叠部分 
	for(int i=0;i<n;i++)
		if(g(i,st1)==g(i,st2))
			return false;
	return true; 
}
void pst(int st){
	for(int i=0,t;i<n;i++){
		t=st%3;
		switch(t){
			case 0:
				cout<<'r';
				break;
			case 1:
				cout<<'e';
				break;
			case 2:
				cout<<'d';
				break;
		}
		st/=3;
	}
}

int main(){
	mp['r']=0;
	mp['e']=1;
	mp['d']=2;
	cin>>n>>m;
	for(int i=0;i<n;i++)
		cin>>s[i];
	maxst=pow(3,n)-1;
	
	for(int st=0;st<=maxst;st++)
		if(check(st))dp[st][1]=mod(1,st);
		else dp[st][1]=114514;
	
//	for(int st=0;st<=maxst;st++){
//		pst(st);
//		printf(" %d\n",dp[st][1]);
//	}
//	puts("");
	
	for(int i=2,t;i<=m;i++){
		for(int st=0,cnt;st<=maxst;st++){
			dp[st][i]=114514;
			if(check(st)){
				cnt=mod(i,st);
				for(int st2=0;st2<=maxst;st2++){
					if(check(st,st2))
						dp[st][i]=min(dp[st][i],dp[st2][i-1]+cnt);
				}
			}
		}
	}
	
	int ans=114514;
	for(int st=0;st<=maxst;st++)
		ans=min(ans,dp[st][m]);
	cout<<ans<<endl;
	
	return 0;
}
相关推荐
爪哇学长27 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己29 分钟前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan31 分钟前
C语言:数组转换指针的时机
c语言·开发语言·算法
繁依Fanyi44 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
烦躁的大鼻嘎1 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝1 小时前
C/C++内存管理
java·c语言·c++
fhvyxyci1 小时前
【C++之STL】摸清 string 的模拟实现(下)
开发语言·c++·string
qq_459730031 小时前
C 语言面向对象
c语言·开发语言
C++忠实粉丝1 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
古月居GYH1 小时前
在C++上实现反射用法
java·开发语言·c++