//写于rating值2184/2184
//上黄场,rating值+152。这一场没有什么算法,靠着一波快手速抢了一波不计div1选手的div2的第25名,记div1选手的第109名。很侥幸,但是上了黄名还是非常开心的。
比赛链接:https://codeforces.ml/contest/1388
A题
简单的构造。应该有很多的过题方法,这里我用的特判的方法,在稿纸上列了一波,6分钟过的题。
给定一个数字n,我们需要构造四个正整数a,b,c,d满足a+b+c+d=n,并且a,b,c,d中至少有三个数字可以表示成p × \times ×q的形式,其中p和q均为质数。
首先我们很容易构造出来,满足可以表示成两个质数相乘的数字中最小的三个为6,10,14。这三个数相加等于30,第四个数字又是正整数,因此当n<=30的时候,我们是无法构造出满足需要的四个正整数的。
而当n的值大于30的时候,我们可以构造6,10,14,n-30这样一个组合。
记rest=n-30,再特判一下rest与6,10,14相等的三种特殊情况。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
if(n<31) cout<<"NO"<<endl;
else
{
cout<<"YES"<<endl;
ll rest=n-30;
if(rest==14)
{
cout<<6<<' '<<9<<' '<<14<<' '<<15<<endl;
}
else if(rest==10)
{
cout<<6<<' '<<9<<' '<<10<<' '<<15<<endl;
}
else if(rest==6)
{
cout<<5<<' '<<6<<' '<<10<<' '<<15<<endl;
}
else cout<<6<<' '<<10<<' '<<14<<' '<<rest<<endl;
}
}
}
B题
简单的构造。
给定一个整数n,我们需要构造一个长度为n的十进制数字x,把x的每一位转化成二进制后重新写出,再扔掉此时二进制的最末尾的n位,我们要使得此时剩下的二进制值最大,并且原本的数字x要尽可能小。
首先我们采取贪心的策略。我们列出0-9这10个数字的二进制表示:
0:0
1:1
2:10
3:11
4:100
5:101
6:110
7:111
8:1000
9:1001
很容易发现只有数字8和9对应的二进制位数是4,由于我们最后需要抹去的长度n已经确定,并且是最末的的n位不会影响到头部造成出现0的情况。因此我们直接贪心选择构造位数尽可能长,也就是选择数字8和9来构造。
我们在保证最后剩下的二进制数值最大的同时,还需保证初始的x的值尽可能小。
我们需要删除最后二进制数的n位,我们的8和9对应的二进制是4位。
对于原数字x最末尾的n/4(向上取整)位的数字来说,选8和选9是没有区别的,最后剩下的值都是一样的
8 删掉一位是100 删掉两位是10 删掉三位是1 删掉四位是空
9 删掉一位是100 删掉两位是10 删掉三位是1 删掉四位是空
因此这末尾的n/4(向上取整)位我们取值更小的8,前面补9使得最后的数字尽可能大。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
ll ed=n/4;
if(n%4) ed++;
ll f=n-ed;
for(ll i=0;i<f;i++) cout<<9;
for(ll i=0;i<ed;i++) cout<<8;
cout<<endl;
}
}
C题
逆向思维,dfs。
给我们一棵树,有n个结点,总共有m个人住在这棵树上。
一开始他们都在结点1,每个人都有一个目标结点,并且他们各自会走最短路。
刚出发的时候他们都有一个心情状态,有些人的心情是好的,有些人的心情是坏的。并且在结点之间转移的时候,原本心情好的人可能变成心情坏,但是心情坏的人就一直都是坏不会转变成好心情。
已知每个结点被多少个人当做目标结点num[i],
现在收集了每个结点的数据happy[i],数据为在上述过程中经过该节点的好心情人数减去坏心情人数的值。
现在需要判断这些数据是否存在错误。
我们反过来思考,不是从结点1到达各自的目标结点,而是从各自的目标结点走到结点1。这样就相当于转化成了一个以结点1为起点的dfs遍历递归过程。
同时对应的原本心情好的人可能变成心情坏这句话就会变成原本心情坏的人可能变成心情好。
我们当前遍历所在结点的所有子树上的节点都是要经过当前节点的,我们通过dfs递归记录当前结点(记为now)的所有子树总共有多少人,加上num[now]即为经过当前结点的总人数,记为sum[now],并且在递归的同时记录所有子树总共有多少心情好的人,记为sumgood[now]。
通过(sum[now]-happy[now])/2+happy[now]可以算出经过now结点有多少心情好的人(要判断(sum[now]-happy[now])/2是否是整数,如果不是整数那必然是数据出错),记为good。由于递归回来的过程中原本心情坏的人可能变成心情好,所以当前结点心情好的人的数量good不能小于所有子树心情好的人的数量sumgood[now]。
上代码吧。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e5+7;
ll n,m;
struct Edge
{
ll to,next;
}edge[maxn*2];
ll head[maxn],tot;
void init()
{
for(ll i=1;i<=n;i++) head[i]=-1;
tot=0;
}
void add(ll u,ll v)
{
edge[tot].to=v;
edge[tot].next=head[u];
head[u]=tot++;
}
//链式前向星存图
bool flag;
ll num[maxn],happy[maxn];
ll sum[maxn],sumgood[maxn];//sum[i]为当前结点与它的所有子节点的总人数,sumgood[i]为当前结点所有子节点的人心情好的人数。
void dfs(ll pre,ll now)//pre为当前结点的递归的前一个结点位置,now为当前结点
{
if(!flag) return;
sum[now]=sumgood[now]=0;
for(ll i=head[now];i!=-1;i=edge[i].next)
{
ll to=edge[i].to;
if(to!=pre) //避免反向走回去
{
dfs(now,edge[i].to);
sum[now]+=sum[to]; //累加所有子树的总人数
sumgood[now]+=sumgood[to]; //累加所有子树心情好的人的数量
}
}
sum[now]+=num[now]; //加上当前结点的人数,即为经过当前的结点的总人数
if(happy[now]>sum[now]||(sum[now]-happy[now])%2) flag=0;//如果所有子节点中心情好的人超出了当前结点经过的总人数
else //或者通过给定数据计算当前结点心情好的人的式子中出现了小数,都代表数据有错
{
ll good=(sum[now]-happy[now])/2+happy[now];//通过给定数据计算当前结点心情好的人的数量
if(good<sumgood[now]) flag=0; //该数值不可以小与所有子树心情好的人的数量总和
else sumgood[now]=good;
}
}
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
cin>>n>>m;
for(ll i=1;i<=n;i++) cin>>num[i];
for(ll i=1;i<=n;i++) cin>>happy[i];
flag=1;
init();
for(ll i=1;i<n;i++)
{
ll u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
flag=1;
dfs(-1,1);
if(flag) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
D题
贪心,拓扑。
给定两列长度都为n的数列a[i]和b[i],我们一开始有一个ans值为0,我们每次选择a[i]中的一个数字,把a[i]加到ans上,同时把a[i]加到a[b[i]]上。
每个下标只能且必须取一次,求最大的ans值,并输出一种取下标的顺序。
我们把i->b[i]看做一个有向边,1到n这n个下标作为结点,就会转化成一个有向图。(并且题目中已经告诉你了不存在闭环)
这个有向图里的每个结点都有一个初始值,每个结点都必须被取一次,被取之后当前结点的值会加到ans上并且会加到有向边所指向的下一个结点的数值上。
我们采取贪心的策略,用ru[i]数组记录第i个结点的入度,那么当前入度为0的结点的数值不会再被改变,如果a[i]是负数,那么我们最后再取它,因为先取的话会让a[b[i]]的数值变小。
如果a[i]是正数,那么我们立刻取它,并把这个值加到a[b[i]]上让它增大。
综上我们直接对整个有向无环图进行一次拓扑排序,利用ans1和ans2两个vector来保存输出顺序。
对于入度为0的下标i,我们把a[i]加到ans上,再根据a[i]的正负进行分类讨论:
如果a[i]是负数,那么我们最后再取它,也就是说它的值不用加到a[b[i]]上,把下标i压入到ans2中。
如果当前结点的值是正数,那么我们立刻取它,也就是说它的值要加到a[b[i]]上,把下标i压入到ans1中。
最后输出顺序的时候
ans1中保存的是正值的下标,直接按照拓扑排序的顺序即可,因为对于这些正数来说,位置靠前的先取会对后面的情况更有利。
ans2中保存的是负值的下标,按照拓扑排序的逆序顺序输出,因为对于这些负数来说,位置靠前的先取会对后面的情况更糟糕。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=2e5+7;
ll n;
ll a[maxn],b[maxn];
ll ru[maxn];//ru[i]保存结点i的入度
int32_t main()
{
IOS;
cin>>n;
for(ll i=1;i<=n;i++) cin>>a[i];
for(ll i=1;i<=n;i++)
{
cin>>b[i];
if(b[i]!=-1) ru[b[i]]++;
}
ll ans=0;
queue<ll>Q;
vector<ll>ans1,ans2;
for(ll i=1;i<=n;i++)
if(!ru[i]) Q.push(i);
while(Q.size())///拓扑排序过程
{
ll now=Q.front();
Q.pop();
ll to=b[now];
if(to!=-1)
{
ru[to]--;
if(!ru[to]) Q.push(to);
if(a[now]>0) a[to]+=a[now];//如果当前结点的值是正数,那么加到下一个对应结点的值上
}
ans+=a[now];//入度为0的结点的值加到ans上
if(a[now]>=0) ans1.push_back(now);
else ans2.push_back(now);
}
cout<<ans<<endl;
for(ll i=0;i<ans1.size();i++) cout<<ans1[i]<<' ';//正数结点的下标按照拓扑序输出
ll len=ans2.size();
for(ll i=0;i<ans2.size();i++) cout<<ans2[len-i-1]<<' ';//负数结点的下标按照拓扑序的逆序输出
cout<<endl;
}