题目整理-5(练习4)
T1、释放囚犯
题面描述:
有 \(n\) 间房子,每个房子中都有一个囚犯,房子按照标号排成一列,同时,相邻的两个房子的人可以传话到更远的房子。
现在,需要释放 \(m\) 个囚犯,标号从 \(a_1\) 到 \(a_m\) ,在释放一个囚犯时,需要给原本可以和他说上话的人一人一份肉吃。
求发的肉的最少份数。
思路:
在释放一个囚犯后,他的左右两边的人的释放顺序就互不影响了。
因此,我们可以轻松想到区间 \(dp\),但是由于细节不好处理,本人选择了记忆化搜索。
标签:
区间dp
代码实现:
this
#include<bits/stdc++.h>
#define N 105
#define INF 0x3f3f3f3f
using namespace std;
int n,m,a[N];
int ans=INF,f[N][N];
int dfs(int i,int j){
if(f[i][j]!=INF) return f[i][j];
if(i==j) return (f[i][j]=a[j+1]-a[i-1]-2);
f[i][j]=min(dfs(i+1,j),dfs(i,j-1));
for(int k=i+1;k<j;k++){
f[i][j]=min(f[i][j],dfs(i,k-1)+dfs(k+1,j));
}
f[i][j]+=a[j+1]-a[i-1]-2;
return f[i][j];
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++) cin>>a[i];
a[m+1]=n+1;
memset(f,0x3f,sizeof(f));
cout<<dfs(1,m);
return 0;
}
T2、色板游戏
题面描述:
给你一个初始都为 \(1\) 的数列,有两个操作如下:
- 将 \([i,j]\) 区间内的数变为 \(k\)
- 统计区间 \([i,j]\) 有多少种数
思路:
观察颜色数量发现,颜色数最大只有 \(30\)!因此,我们可以对颜色进行状压,配合线段树高效解决此题。
标签:
线段树
状态压缩
代码实现:
this
#include<bits/stdc++.h>
#define N 100005
#define lowbit(x) (x&(-x))
#define il inline
using namespace std;
template<class T,int count>
struct x_tree{
x_tree<T,count-1> s[2];
#define mid (1<<(count-1))
#define len (1<<count)
T sm,lz;
il x_tree(){sm=lz=0;}
il void x_add(T k){sm=lz=k;}
il void push_up(){sm=s[0].sm|s[1].sm;}
il void push_down(){
if(!lz) return;
s[0].x_add(lz);
s[1].x_add(lz);
lz=0;
}
il void add(int l,int r,T k){
if(l<=1&&len<=r) x_add(k);
else{
push_down();
if(mid>=l) s[0].add(l,r,k);
if(mid<r) s[1].add(l-mid,r-mid,k);
push_up();
}
}
il T sum(int l,int r){
if(l<=1&&len<=r) return sm;
push_down();
T ret=0;
if(mid>=l) ret|=s[0].sum(l,r);
if(mid<r) ret|=s[1].sum(l-mid,r-mid);
return ret;
}
#undef len
#undef mid
};
template<class T>
struct x_tree<T,0>{
T sm;
il x_tree(){sm=0;}
il void x_add(T k){sm=k;}
il void add(int l,int r,T k){sm=k;}
il T sum(int l,int r){return sm;}
};
x_tree<int,17> t;
int n,m,T;
char c;
int num_2(int k){
int ret=0;
while(k) ret++,k-=lowbit(k);
return ret;
}
int main(){
cin>>n>>m>>T;
t.add(1,n,1);
while(T--){
int l,r,k;
cin>>c>>l>>r;
if(l>r) swap(l,r);
if(c=='C'){
cin>>k;
t.add(l,r,1<<(k-1));
}
else cout<<num_2(t.sum(l,r))<<"\n";
}
return 0;
}
T3、小 a 和 uim 之大逃离
题面描述:
有一个 \(n\times m\) 的巨幅矩阵,矩阵的每个格子上有一坨 \(0\sim k\) 不等量的魔液。
小 a 和 uim 各有一个魔瓶,他们可以从矩阵的任一个格子开始,每次向右或向下走一步,从任一个格子结束。开始时小 a 用魔瓶吸收地面上的魔液,下一步由 uim 吸收,如此交替下去,并且要求最后一步必须由 uim 吸收。魔瓶只有 \(k\) 的容量,也就是说,如果装了 \(k+1\) 那么魔瓶会被清空成零,如果装了 \(k+2\) 就只剩下 \(1\),依次类推。
请你统计走到最后一步,他俩的魔瓶中魔液一样多有多少种方法。由于可能很大,输出对 \(1,000,000,007\) 取余后的结果。
思路:
非常典型的坐标 \(dp\)。
设 \(f0_{i,j,k_1,k_2}\) 和 \(f1_{i,j,k_1,k_2}\) 分别表示走到了 \((i,j)\) 时,小 a 和 uim 分别有 \(k_1\) 和 \(k_2\) 的魔液,且最后一步是 uim 走的的方案数,易得状态转移方程。同时注意常数即可。
具体看代码。
标签:
坐标dp
代码实现:
this
#include<bits/stdc++.h>
#define N 805
#define mod 1000000007
using namespace std;
int n,m,k,a[N][N];
int ans,f0[2][N][16][16],f1[2][N][16][16];
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) cin>>a[i][j];
int t1,t2;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
++f1[i&1][j][a[i][j]][0]==mod?f1[i&1][j][a[i][j]][0]=0:0;//滚动数组预处理
for(int k1=0;k1<=k;k1++){
(ans+=f0[i&1][j][k1][k1])%=mod;//在滚动前统计答案
for(int k2=0;k2<=k;k2++){
t1=k1+a[i][j+1],t2=k2+a[i][j+1];//提前计算,节省时间
t1>k?t1-=k+1:0;
t2>k?t2-=k+1:0;
(f1[i&1][j+1][t1][k2]+=f0[i&1][j][k1][k2])%=mod;//状态转移
(f0[i&1][j+1][k1][t2]+=f1[i&1][j][k1][k2])%=mod;//状态转移
t1=k1+a[i+1][j],t2=k2+a[i+1][j];//提前计算,节省时间
t1>k?t1-=k+1:0;
t2>k?t2-=k+1:0;
(f1[(i&1)^1][j][t1][k2]+=f0[i&1][j][k1][k2])%=mod;//状态转移
(f0[(i&1)^1][j][k1][t2]+=f1[i&1][j][k1][k2])%=mod;//状态转移
f0[i&1][j][k1][k2]=f1[i&1][j][k1][k2]=0;//滚动数组,节省空间
}
}
}
}
cout<<ans<<"\n";
return 0;
}
T4、树的重量
题面描述:
给出你一个边权全非负的无根树上所有叶子节点之间的距离,求这棵树的边权之和。
同时,保证 \(dis_{i,j}+dis_{j,k}\geq dis_{i,k}\)。
思路:
先说一个结论:在这个无根树中,从任意一个叶节点出发,经过所有的叶节点并回到该节点,最短路径为这棵树的边权和的两倍,即,会经过所有的边恰好两次。
接下来不严谨证明一下。
考虑将这颗无根树以一个非叶子节点为根,使其为一颗有根树。接下来,从画出的最左边出发,依次向右走到下一个节点,举个例子如下:
从 \(5\) 出发,依次走最短路到 \(6,10,3,8,10\),最后再从 \(10\) 走最短路回到 \(5\) ,易发现,每一条边都经过了两遍。
易得,这种方法走为最优解。
得到了这个结论后,结合样例并思考得:这不就是给出一个 \(n\) 个点的无向图,求出经过 \(n\) 个点得最小环吗?因此,完结撒花......就怪了。
我们还不清楚如何求出这个环。结合一定思考并结合性质,可得:
假设此时已经处理出了经过前 \(k\) 个点的最小环,那么可以枚举之前加入环的边,将其=断开,并将原本这条边的两个端点分别连接到 \(k+1\) 上,找到增加最小的一条边并断开连接,一直向下处理即可。
真的易证
代码实践不难。
标签:
贪心
最小环
代码实现:
this
#include<bits/stdc++.h>
#define N 35
using namespace std;
int n,a[N][N],vis[N][N],ans;
int main(){
cin>>n;
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++) cin>>a[i][j];
ans+=a[1][2]+a[2][3]+a[1][3];
vis[1][2]=vis[2][3]=vis[1][3]=1;
for(int k=4;k<=n;k++){
int t1=0,t2=0;
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++){
if((vis[i][j]&&a[i][k]+a[j][k]-a[i][j]<a[t1][k]+a[t2][k]-a[t1][t2])||t1==0) t1=i,t2=j;
}
vis[t1][t2]=0,vis[t1][k]=vis[t2][k]=1;
ans+=a[t1][k]+a[t2][k]-a[t1][t2];
}
cout<<ans/2;
return 0;
}
T5、[ROIR 2022] 分数排序 (Day 2)
题面描述:
有两个由 \(n\) 个不同整数组成的序列 \(A = [a_1, a_2, \dots , a_n]\) 和 \(B = [b_1, b_2, \dots , b_n]\)。将它们组合成 \(n^2\) 个分数,形式为 \(\frac{a_i}{b_j}\),并将每个分数约分后按递增顺序排序。
给定一个数字 \(q\) 和 \(q\) 个整数 \(c_1, c_2, \dots , c_q\)。对于每个 \(c_i\),请输出上面所说的 \(n^2\) 个分数中第 \(c_i\) 小的分数。
思路:
先将 \(a\) 和 \(b\) 排序。
可以发现,当这个分数的值越大,它的排名也越大(这不是废话吗),同时,求一个分数的排名也十分容易,只需要计算出对于每个 \(b_j\) 有多少个 \(\frac{a_i}{b_j}\) 比它小就好了(不过需要注意的是,可能有多个相同值的分数,事实上我们求出来的只能是一个排名区间),而且时间复杂度也很优,\(q\)次的提问复杂度仅为 \(qn\log_2(n)\)(因为 \(qn<10^5\)),所以,结合单调性,我们通过实数二分得到排名 \(c_i\) 的数。
接下来就简单了,我们只需要枚举每个 \(b_j\) 二分找出满足要求的 \(a_i\),最后取所有答案中误差最小的的即可。
标签:
实数二分
二分
代码实现:
this
#include<bits/stdc++.h>
#define N 100005
#define ll long long
#define eps 1e-12
using namespace std;
ll n,q,a[N],b[N],c;
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
double jue(double x){return x>=0?x:-x;}
ll ck(double md){
ll ln=0,rn=0,l=1,r=n;
for(ll j=1;j<=n;j++){
r=n;
while(l<r){
ll mid=(l+r)/2;
(a[mid]*1.0>md*b[j])?r=mid:l=mid+1;
}
if(a[l-1]*1.0<=md*b[j]) rn+=l-1;
if(a[l]*1.0<=md*b[j]) rn++;
l=1;
while(l<r){
ll mid=(l+r+1)/2;
(a[mid]*1.0<md*b[j])?l=mid:r=mid-1;
}
if(a[l]*1.0<md*b[j]) ln+=l;
}
return rn>=c;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>q;
for(ll i=1;i<=n;i++) cin>>a[i];
for(ll i=1;i<=n;i++) cin>>b[i];
sort(a+1,a+n+1);
sort(b+1,b+n+1);
while(q--){
cin>>c;
double lc=0,rc=1e6;
for(int kkk=1;kkk<=1000&&rc-lc>eps;kkk++){//注意实数二分的次数
double mdc=(lc+rc)/2;
(ck(mdc))?rc=mdc:lc=mdc;
}
ll asi=n,asj=1;
double mn=1e6;
for(ll j=(c*2<=n*n)?n:1;j<=n&&j;j+=(c*2<=n*n)?-1:1){//b_j
ll la=1,ra=n;
while(la<ra){
ll mid=(la+ra)/2;
(b[j]*lc<=a[mid])?ra=mid:la=mid+1;
}
if(jue(a[la]-b[j]*lc)<=mn) asi=la,asj=j,mn=jue(a[la]-b[j]*lc);
}
ll g=gcd(a[asi],b[asj]);
cout<<a[asi]/g<<" "<<b[asj]/g<<"\n";
}
return 0;
}
T6、[GDKOI2024 普及组] 刷野 I
题面描述:
Z 是一个与怪物战斗的巫师,这次他将面临 \(n\) 个站成一排的怪物,其中第 \(i\) 个怪物的生命值是 \(a_i\)。
Z 率先使用一种攻击方式攻击,攻击过后所有血量小于等于 \(0\) 的怪物死亡。在 Z 攻击一次后,所有存活的怪物对 Z 造成 \(1\) 点伤害。以上步骤不断循环,直到 Z 击杀所有怪物为止。
Z 一共有三种攻击方式:
-
普通攻击: 消耗 \(0\) 点能量值,选择一只怪物并使其血量减少一点。
-
天音波: 消耗 \(1\) 点能量值,选择一只怪物并使其血量减少两点。
-
天雷破: 消耗 \(1\) 点能量值,使所有怪物血量减少一点。
现在 Z 一共有 \(m\) 点能量,现在他想知道在最优的策略下,击败 \(n\) 只怪物所损失的最少血量。
思路:
记天音波为 \(c_1\),天雷破为 \(c_2\)。
首先贪心易得,当不进行 \(c_1\) 和 \(c_2\) 时,将 \(a_i\) 从小到大排序后进行攻击最优。所以此时他的答案为:
因此,我们可以将对 \(a_i\) 使用一次 \(c_1\) 看成将 \(a_i\) 减去二,将使用一次 \(c_2\) 看成将在场所有的 \(a_i\) 都减去一。
最后,依照上面的式子可以得到如下的贪心策略:
- 有能量就要放大招。
- 对于一个怪物,要么只使用 \(c_1\),要么只使用 \(c_2\)(当 \(a_i\) 为奇数且只使用 \(c_1\) 时,将最后一次血量只剩 \(1\) 的 \(c_1\) 要换为 \(c_2\))。
标签:
贪心
代码实现:
this
#include<bits/stdc++.h>
#define N 100005
#define ll long long
using namespace std;
ll n,m,a[N],ans;
int main(){
cin>>n>>m;
ans-=n;
for(ll i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1);
ll c1=0,c2=0;
for(ll i=1;i<=n;i++){
if(a[i]<=c2) continue;
ll t2=0,t1=n-i+1,ic1=0,ic2=c2,lst=0;
while(a[i]>2*ic1+c2&&c1+c2<m){
if(a[i]-ic1*2-ic2==1){
c2++;
break;
}
if(!lst){
t2=0,t1=n-i+1;
for(ll j=i+1;j<=n;j++) if(a[j]>c2) t2+=n-j+1;
if(t2>=t1) c2++,lst=2;
else c1++,ic1++,lst=1;
}
else{
if(lst==2) c2++;
else c1++,ic1++;
}
}
a[i]-=ic2+ic1;
ans+=a[i]*(n-i+1);
}
cout<<ans<<"\n";
return 0;
}
T7(待完善)、「HCOI-R1」孤独的 sxz
题面描述:
给你一个 \(n\times m\) 的网格和 \(k\) 个不重复的点 \((x_i,y_i)\) ,请你找出异于这 \(k\) 个点的一个点 \((a,b)\),使如下式子最大:
思路:
将 \(x,y\) 分开排序。
将上述求和拆开得到:
发现形式相同,故暂时只对一个式子讨论。
设前 \(t_1\) 个 \(x_i\) 都比 \(a\) 小且 \(t_1\) 后的 \(x_i\) 都比 \(a\) 大,则式子可以化为:
可以发现,当 \(a\) 取得最大或者最小值时,该式取得最大值。
右边的式子同理。
所以,我们对这个矩阵的左上、左下、右上、右下这四个角角的四个边长为 \(1000\) 的正方形内进行遍历并依靠前缀和求出答案,并更新即可。
标签:
数学
代码实现:
this
#include<bits/stdc++.h>
#define ll long long
#define pr pair<ll,ll>
#define mr(a,b) make_pair(a,b)
#define K 400005
using namespace std;
ll n,m,k,ans,x[K],y[K];
map<pr,ll> vis;
ll solve(int i,int j){
if(vis.count(mr(i,j))) return 0;
ll l=1,r=k,ret=0;
#define mid ((l+r+1)>>1)
while(l<r) (x[mid]-x[mid-1]<=i)?(l=mid):(r=mid-1);
if(l==1&&x[1]>i) l=0;
ret+=(2*l-k)*i+x[k]-2*x[l];
l=1,r=k;
while(l<r) (y[mid]-y[mid-1]<=j)?(l=mid):(r=mid-1);
if(l==1&&y[1]>j) l=0;
return ret+(2*l-k)*j+y[k]-2*y[l];
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=k;i++){
cin>>x[i]>>y[i];
vis[mr(x[i],y[i])]=1;
}
sort(x+1,x+k+1);
sort(y+1,y+k+1);
for(int i=1;i<=k;i++) x[i]+=x[i-1],y[i]+=y[i-1];
int k1=min(1000ll,n),k2=min(1000ll,m);
for(int i=1;i<=k1;i++){
for(int j=1;j<=k2;j++){
ans=max(ans,solve(i,j));
}
for(int j=m,nj=1;nj<=k2;j--,nj++){
ans=max(ans,solve(i,j));
}
}
for(int i=n,ni=1;ni<=k1;i--,ni++){
for(int j=1;j<=k2;j++){
ans=max(ans,solve(i,j));
}
for(int j=m,nj=1;nj<=k2;j--,nj++){
ans=max(ans,solve(i,j));
}
}
cout<<ans<<"\n";
return 0;
}
注:
- 题号为本校OJ上的链接,题名为原出处链接。


浙公网安备 33010602011771号