练习二题目记录
A.[POI2004] SZP
Description
给你一个有向有环图,每个节点只能也必须发出一条非自环的边,要求选择尽可能多的点,使得选中的每一个点都至少有一个指向它的节点没有被选中,求可以选择的最多的点数。
Solution
易得,在每一个联通块中,有且仅有一个环。考虑到每个节点只能发出一条边,那么我们可以将所有边倒过来建,这样每个联通块就形成了一棵基环树,整张图就形成了一个基环树森林。
关注每一个联通块,我们可以先删掉环上的一条边,这样就形成了一个正常的树。然后我们以删掉的边的一个节点为根跑树形 DP,然后再将删除的这个边连上,进行一次换根。
先看 DP,设 \(dp_{x,0/1}\) 表示在以 \(x\) 为根的子树中,选或不选 \(x\) 节点所能选择的最多的节点数量,易证:\(dp_{x,1}\) 一定大于 \(dp_{x,0}\)。设 \(y\) 为 \(x\) 的子节点,转移方程如下:
最后根据题意进行一次换根即可,换根的时候需要关注根节点通过被断开的边到达的点的状态,所以我们需要求出当根节点不选择时的每个节点的状态。因为当一个节点选择时,只有一个子节点不选,所以设 \(pre_i\) 表示在选择这个节点时不选的那个子节点,然后再遍历一遍处理出状态即可。因为这题的空间卡得很紧,没法建边,所以我们记录每个节点的父节点,然后通过拓扑排序处理出转移的顺序,最后按照这个顺序,从 \(x\) 转移到 \(fa_x\),处理状态时反过来即可。
Code
#include<bits/stdc++.h>
#define N 1000005
using namespace std;
int n,f[N],fa[N],dp[N][2],ot[N],ans,otn[N],pre[N],nm[N];
vector<int>rt;
int findf(int x){
if(x==f[x])return x;
return f[x]=findf(f[x]);
}
void tuopu(){
memset(f,0,sizeof f);
for(int i=1;i<=n;i++)if(!ot[i])f[++f[0]]=i;
for(int i=1;i<=f[0];i++){
if(!fa[f[i]])continue;
--ot[fa[f[i]]];
if(!ot[fa[f[i]]])f[++f[0]]=fa[f[i]];
}
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=n;i++){
cin>>fa[i];
if(findf(i)==findf(fa[i])){
rt.push_back(i);
nm[fa[i]]=1;
}
else{
f[i]=fa[i];
++ot[fa[i]];
++otn[fa[i]];
}
}
tuopu();
memset(ot,0x3f,sizeof ot);
for(int i=1;i<=n;i++){
int x=f[i];
if(otn[x]>0)dp[x][1]-=ot[x]-1;
dp[fa[x]][0]+=dp[x][1];
dp[fa[x]][1]+=dp[x][1];
if(ot[fa[x]]>=dp[x][1]-dp[x][0]){
if(ot[fa[x]]==dp[x][1]-dp[x][0]){
if(!nm[x])pre[fa[x]]=x;
}
else{
ot[fa[x]]=dp[x][1]-dp[x][0];
pre[fa[x]]=x;
}
}
}
memset(nm,0,sizeof nm);
for(int i=n;i;i--){
int x=f[i];
if(count(rt.begin(),rt.end(),x)||!otn[x]){
nm[x]=0;
continue;
}
if(!nm[fa[x]]||(nm[fa[x]]&&pre[fa[x]]!=x))nm[x]=1;
}
for(int p:rt){
ans+=max(dp[p][1],dp[p][0]+(pre[fa[fa[p]]]!=fa[p]&&!nm[fa[p]]));
}
cout<<ans;
return 0;
}
B.元旦晚会
Description
给你一个长为 \(n\) 的数组和 \(m\) 个区间,让你在数组中选一些数,使得区间 \(i\) 中至少有 \(c_i\) 个元素被选中,求最少需要被选中多少个数。
Solution
贪心。将这 \(m\) 个区间以左端点为第一关键字,以右端点为第二关键字排序,然后从后往前贪即可。选择的时候尽可能去找重复的地方,即从前往后找。
Code
#include<bits/stdc++.h>
#define N 30005
#define M 5005
using namespace std;
int n,m,ans,vis[N];
struct node{
int l,r,x;
bool operator<(const node x){
if(l!=x.l)return l<x.l;
return r<x.r;
}
}a[M];
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>a[i].l>>a[i].r>>a[i].x;
sort(a+1,a+1+m);
for(int i=m;i;i--){
for(int j=a[i].r;j>=a[i].l&&a[i].x;j--){
if(vis[j])--a[i].x;
}
ans+=a[i].x;
for(int j=a[i].l;j<=a[i].r&&a[i].x;j++){
if(!vis[j])vis[j]=1,--a[i].x;
}
}
cout<<ans;
return 0;
}
C.田忌赛马
Description
给你长度为 \(n\) 的一个 \(a\) 数组和一个 \(b\) 数组,让你选择一种方案,使 \(a_i\) 和 \(b_j\) 一一对应,当 \(a_i>b_i\) 时获得 200 银币,\(a_i=b_i\) 时不获得银币,\(a_i<b_i\) 时减去 200 银币。
Solution
将 \(a\) 和 \(b\) 按从大到小排序,考虑贪心。从前往后遍历 \(b\),对于 \(b_i\) 有两种选择,一是用当前最大的 \(a_i\) 赢他然后获得 200 银币,二是用当前最小的 \(a_i\) 耗死他然后减去 200 银币。所以到 \(b_i\) 时,\(a\) 一定被分成了三部分,第一和第三部分被选择了,中间的部分空着。
设 \(dp_{i,j}\) 表示选择到 \(b_i\) 时,\(a\) 前面那一部分被选择了 \(j\) 个。转移具体看代码。
Code
#include<bits/stdc++.h>
#define N 2005
using namespace std;
int n,a[N],b[N],dp[N][N],ans;
int cmp(int a,int b){
return a>b;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
sort(a+1,a+1+n,cmp);
sort(b+1,b+1+n,cmp);
memset(dp,128,sizeof dp);
dp[0][0]=0;
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++){
int k=i-j;
if(a[j+1]>b[i+1])dp[i+1][j+1]=max(dp[i+1][j+1],dp[i][j]+200);
else if(a[j+1]<b[i+1])dp[i+1][j+1]=max(dp[i+1][j+1],dp[i][j]-200);
else dp[i+1][j+1]=max(dp[i+1][j+1],dp[i][j]);
/*------------------------------------------*/
if(a[n-k]>b[i+1])dp[i+1][j]=max(dp[i+1][j],dp[i][j]+200);
else if(a[n-k]<b[i+1])dp[i+1][j]=max(dp[i+1][j],dp[i][j]-200);
else dp[i+1][j]=max(dp[i+1][j],dp[i][j]);
}
}
for(int i=0;i<=n;i++)ans=max(ans,dp[n][i]);
cout<<ans;
return 0;
}
D.[APIO/CTSC2007] 数据备份
Description
现在在一条数轴上有 \(n\) 个楼房,你可以将两座楼房连接,其代价就是两楼之间的距离。现在你需要连接 \(K\) 次,被连接的楼房只能与别的节点连一次,求最小的代价。
Solution
首先我们需要知道一个东西,就是最终选择连接的两座楼房一定是相邻的,所以我们先预处理出来 \(a_i\),表示 \(i\) 和 \(i-1\) 座楼房的距离,即选择 \(i\) 的代价,问题也就转换成了选择 \(a_i\)。因为每个楼房最多只能连接一次,所以选择的每一个 \(a_i\) 不能相邻。
考虑 DP。设 \(dp_{i,k}\) 表示在选择第 \(i\) 个并在此之前包括 \(i\) 一共选择了 \(k\) 个。转移方程即为:
我们关注到 \(k\) 只能由第 \(k-1\) 转移,所以我们可以滚动数组,然后再用前缀最小值优化一下即可。
Code
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,m,a[N],dp[N][2],ans=0x3f3f3f3f;
signed main(){
cin>>n>>m;
for(int i=2;i<=n+1;i++)cin>>a[i],a[i-1]=a[i]-a[i-1];
memset(dp,0x3f,sizeof dp);
dp[0][0]=dp[0][1]=0;
for(int k=1;k<=m;k++){
int num=0x3f3f3f3f;
for(int i=2*k;i<=n-(m-k)*2;i++){
num=min(num,dp[i-2][!(k&1)]);
dp[i][k&1]=num+a[i];
}
}
for(int i=2*m;i<=n;i++)ans=min(ans,dp[i][m&1]);
cout<<ans;
return 0;
}
E.[TJOI2013] 拯救小矮人
Description
一群小矮人掉进了一个很深的陷阱里,由于太矮爬不上来,于是他们决定搭一个人梯。即:一个小矮人站在另一小矮人的 肩膀上,直到最顶端的小矮人伸直胳膊可以碰到陷阱口。
对于每一个小矮人,我们知道他从脚到肩膀的高度 \(A_i\),并且他的胳膊长度为 \(B_i\)。陷阱深度为 \(H\)。
如果我们利用矮人 \(1\),矮人 \(2\),矮人 \(3\),……,矮人 \(k\) 搭一个梯子,满足 \(A_1+A_2+A_3+\dots+A_k+B_k \geq H\),那么矮人 \(k\) 就可以离开陷阱逃跑了,一旦一个矮人逃跑了,他就不能再搭人梯了。求最多能有几个小矮人跑出来。
Solution
这道题是一道贪心+DP。按照 \(a_i+b_i\) 从小往大排序,然后从前往后跑背包 DP。为什么这么贪心呢?因为 \(a_i+b_i\) 越小,他要出来底下的人梯就要越高,也就要越先出来。
设 \(dp_{i,j}\) 表示到 \(i\) 时剩下的人梯高度为 \(j\) (不包括手臂长度)的最多出来的小矮人数量。转移方程如下:
最后滚动数组即可。
Code
#include<bits/stdc++.h>
#define N 2005
#define M 400005
using namespace std;
int n,m,sum,ans,dp[2][M*2];
struct node{
int a,b;
bool operator<(const node x)const{
if(a+b!=x.a+x.b)return (a+b)<(x.a+x.b);
if(a!=x.a)return a<x.a;
return b<x.b;
}
}x[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>x[i].a>>x[i].b,sum+=x[i].a;
sort(x+1,x+1+n);
cin>>m;
for(int i=1;i<=n;i++){
if(x[i].a+x[i].b>=m){
ans+=n-i+1;
cout<<ans;
return 0;
}
memcpy(dp[i&1],dp[!(i&1)],sizeof dp[!(i&1)]);
for(int j=sum-x[i].a+M;j>=m-x[i].b-x[i].a+M;j--){
dp[i&1][j]=max(dp[!(i&1)][j],dp[!(i&1)][j+x[i].a]+1);
ans=max(ans,dp[i&1][j]);
}
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号