[54] (多校联训) A层冲刺NOIP2024模拟赛12
加把劲
加把劲
5k
加把劲
加把劲
加把劲
加把劲
快 CSP-S 了
加把劲 加把劲
省流:
B 先生推行了一项措施,这项措施里有一部分 \(a\) 是对的,另一部分 \(b\) 是错的,钦定 \(|a|\lt|b|\)
如果 A 不满意 B 先生的措施,可以这样做:
- 直接向 B 先生寻求探讨:A 向 B 先生理性阐述了 \(b\) 部分的危害,B 先生表示已经知道 \(b\) 部分的存在,同时反复强调 \(a\) 的重要性,A 被 B 先生告知不喜欢可以滚蛋,A 被 B 先生完爆
- 向 B 先生跳脸:A 将遭受 B 先生强度更大的跳脸,A 被 B 先生完爆
- 尝试独立:A 被 B 先生告知别人都能忍,为什么你忍不了,最终 B 先生以从众为由强制 A 服从,A 被 B 先生完爆
总结:B 先生一意孤行,意图在全社会推行帝国主义制度,复辟帝制
推论:无敌的傻逼是最可怕的
A.Alice 和璀璨花
\(n^3\) 的一个做法
设 \(f_{i,j}\) 表示选了 \(i\) 个,上一个选了 \(j\)
#include<bits/stdc++.h>
using namespace std;
#define int long long
template<typename T>void ignored(T x){}
int n;
int a[1001];
int b[1001];
int f[1001][1001];
signed main(){
ignored(freopen("alice.in","r",stdin));
ignored(freopen("alice.out","w",stdout));
ignored(scanf("%lld",&n));
for(int i=1;i<=n;++i){
ignored(scanf("%lld",&a[i]));
}
for(int i=1;i<=n;++i){
ignored(scanf("%lld",&b[i]));
}
for(int i=0;i<=n;++i){
for(int j=0;j<=n;++j){
f[i][j]=-0x3f3f3f3f3f3f3f3f;
}
}
int ans=1;
for(int i=1;i<=n;++i) f[0][i]=0;
for(int i=1;i<=n;++i) f[1][i]=1;
for(int i=2;i<=n;++i){
for(int j=i;j<=n;++j){
for(int k=i-1;k<j;++k){
if(a[k]*b[i-1]<a[j]){
f[i][j]=max(f[i][j],f[i-1][k]+1);
ans=max(ans,f[i][j]);
}
}
}
}
cout<<ans<<'\n';
}
题解 \(n^2\) 做法
设 \(f_{i,j}\) 表示考虑到第 \(i\) 个数,当前选择的长度为 \(j\) 的最小值
这个状态设计和最长上升子序列很像,这么做的正确性可以考虑贪心证明
那么 \(f_{i,j}\) 的转移有两种来源:
- 不选 \(a_i\),从 \(f_{i-1,j}\) 转移来
- 选 \(a_i\),从 \(a_i\) 转移来
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
template<typename T>void ignored(T x){}
int n;
int a[1001],b[1001];
int f[1001][1001];
signed main(){
freopen("alice.in","r",stdin);
freopen("alice.out","w",stdout);
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
}
for(int i=1;i<=n;++i){
scanf("%lld",&b[i]);
}
memset(f,0x3f,sizeof f);
for(int i=0;i<=n;++i) f[i][0]=0;
for(int i=1;i<=n;++i){
for(int j=1;j<=i;++j){
f[i][j]=f[i-1][j];
if(f[i-1][j-1]<0x3f3f3f3f3f3f3f3f and a[i]>f[i-1][j-1]*b[j-1]){
f[i][j]=min({f[i][j],a[i]});
}
}
}
for(int i=n;i>=1;--i){
for(int j=1;j<=n;++j){
if(f[j][i]<0x3f3f3f3f3f3f3f3f){
cout<<i<<endl;
return 0;
}
}
}
}
同理,可以压到一维
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
template<typename T>void ignored(T x){}
int n;
int a[1000001],b[1000001];
int f[1000001];
signed main(){
freopen("alice.in","r",stdin);
freopen("alice.out","w",stdout);
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
}
for(int i=1;i<=n;++i){
scanf("%lld",&b[i]);
}
memset(f,0x3f,sizeof f);
f[0]=0;
for(int i=1;i<=n;++i){
for(int j=i;j>=1;--j){
if(f[j-1]<0x3f3f3f3f3f3f3f3f and a[i]>f[j-1]*b[j-1]){
f[j]=min(f[j],a[i]);
}
}
}
for(int i=n;i>=1;--i){
if(f[i]<0x3f3f3f3f3f3f3f3f){
cout<<i<<endl;
return 0;
}
}
}
压到一维之后我们发现,\(f\) 数组始终是单调的,这意味着我们可以直接在 \(f\) 上二分找值,\(n\log n\) 就做完了
#include<bits/stdc++.h>
using namespace std;
#define int long long
template<typename T>void ignored(T x){}
int n;
int a[1000001],b[1000001];
int f[1000001];
set<int>s;
signed main(){
freopen("alice.in","r",stdin);
freopen("alice.out","w",stdout);
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
}
for(int i=1;i<=n;++i){
scanf("%lld",&b[i]);
}
memset(f,0x3f,sizeof f);
f[0]=0;
for(int i=1;i<=n;++i){
int tmp=upper_bound(f+1,f+i+1,a[i])-f-1;
if(f[tmp]<0x3f3f3f3f3f3f3f3f and a[i]>f[tmp]*b[tmp]){
f[tmp+1]=min(f[tmp+1],a[i]);
}
}
for(int i=n;i>=1;--i){
if(f[i]<0x3f3f3f3f3f3f3f3f){
cout<<i<<endl;
return 0;
}
}
}
B.Bob 与幸运日
D.David 与和谐号
赛时想到迭代加深了,但是不会剪枝
赛时裸的迭代加深贴一下
点击查看代码
#include<bits/stdc++.h>
using namespace std;
template<typename T>void ignored(T x){}
int n;
int a[30],b[30];
bool check(){//判断当前状态是否合法
for(int i=1;i<=n;++i){
if(a[i]!=i) return false;
}
return true;
}
bool dfs(int now,int lim){//当前旋转了 now 次,旋转上限是 lim
if(now>lim) return false;
if(check()) return true;
for(int i=2;i<=n;++i){
std::reverse(a+1,a+i+1);
if(dfs(now+1,lim)) return true;
std::reverse(a+1,a+i+1);
}
return false;
}
bool check(int ans){
return dfs(0,ans);
}
int main(){
ignored(freopen("david.in","r",stdin));
ignored(freopen("david.out","w",stdout));
int t;cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i){
cin>>b[i];
}
int ans=0;
while(1){
for(int i=1;i<=n;++i){
a[i]=b[i];
}
if(check(ans)){//从小到大枚举旋转次数,判断是否合法
cout<<ans<<'\n';
break;
}
ans++;
}
}
}
迭代加深有点像广搜,为什么你深搜做不了最短路径的题,是因为深搜第一次碰到的合法状态不一定是最小的,只是最先碰到的,这个最先碰到的状态可能是一个比较深的节点
我们为了能让深搜做出这种最短路径题,可以对值域 \(V\) 做 \(V\) 遍深搜(有单调性亦可二分答案),每次限值搜索的深度,找出什么时候合法,这就是迭代加深
迭代加深效率显然是不如广搜的,但是它具有深搜那种可以直接在搜索中修改的优点,因此用的也比较多
这道题的剪枝
- 可行性剪枝:题解的大力剪枝:每次翻转只会改变一对相邻数对,也就是说如果两个相邻的值在值域上不相邻,那么就一定要通过至少一次旋转来解决(值域相邻的值不一定需要对每个断点应用一次旋转,比如 321 可以只旋转一次到位,这是因为其满足了旋转后有序的必要条件,即旋转前也是有序的,但如果存在值域不相邻的数,如 2413 则一定需要对每个断点至少一次旋转)
- 启发式剪枝:发现转一次之后再马上转回来一定不优,因此判掉这个枝
#include<bits/stdc++.h>
using namespace std;
template<typename T>void ignored(T x){}
int n;
int a[30],b[30];
inline int check(){
int res=0;
for(int i=1;i<=n-1;++i){
if(abs(a[i]-a[i+1])!=1) res++;
}
return res+(a[n]!=n); //这里一定要注意 n 是不是在自己位置上,可能导致下面 tmp==0 错判
}
bool dfs(int now,int lim,int last){
int tmp=check();
if(now+tmp>lim) return false;
if(now==lim) return tmp==0;
for(int i=2;i<=n;++i){
if(i!=last){
std::reverse(a+1,a+i+1);
if(dfs(now+1,lim,i)) return true;
std::reverse(a+1,a+i+1);
}
}
return false;
}
inline bool check(int ans){
return dfs(0,ans,0);
}
int main(){
ignored(freopen("david.in","r",stdin));
ignored(freopen("david.out","w",stdout));
int t;scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&b[i]);
}
int ans=0;
while(1){
for(int i=1;i<=n;++i){
a[i]=b[i];
}
if(check(ans)){
cout<<ans<<'\n';
break;
}
ans++;
}
}
}