这场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;
}