25-暑期-来追梦noip-卷2 总结
开题顺序:A-B-C-D
时间分配:A 1h(1h 怒砍 60pts 也是无敌了)B 30min C 25min D 5min
A
预估 60,实际 60。
把 \(a_i\) 看作价值,\(b_i\) 看作体积,本题转化为背包问题,背包容量显然为 \(100\)。
令 \(dp_{i,j}\) 表示前 \(i\) 次攻击,且魔法值为 \(j\) 的能打掉的最大血量。
在 \(dp_{i,j} \ge 100\) 时取得答案即可。初始全为 \(0\)。
转移:\(dp_{i,j}=\max(dp_{i,j},dp_{i-1,j-a_k+t}+b_k)\),注意条件为 \(j \ge a_k\),还有魔法值要和 \(100\) 取 \(\min\)。
时间复杂度是 \(\mathcal{O}(n^3)\) 的。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
int _,n,t,q;
int a[N],b[N],dp[N][N];
void solve(){
memset(dp,0,sizeof dp);
cin>>n>>t>>q;
for(int i=1;i<=n;i++)
cin>>a[i]>>b[i];
a[0]=0,b[0]=1;
int limit=(100+q-1)/q;
bool f=0;
int ans=-1;
for(int i=1;i<=limit;i++){
for(int k=0;k<=n;k++){
for(int j=0;j<=100;j++){
if(j>=a[k])
dp[i][min(100ll,j-a[k]+t)]=max(dp[i][min(100ll,j-a[k]+t)],dp[i-1][j]+b[k]);
if(dp[i][min(100ll,j-a[k]+t)]>=100){
ans=i,f=1; break;
}
}
if(f) break;
}
if(f) break;
}
if(f)
cout<<ans<<'\n';
else
cout<<"Loser!\n";
}
signed main(){
//freopen("T1.in","r",stdin);
//freopen("T1.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cin>>_;
while(_--)
solve();
return 0;
}
总结:
- 背包问题关注价值、体积和背包容量。
B
预估 30,实际 30。
看到 \(A^3 \times B=N\) 这种乘积式容易想到质因数分解。
然后我们发现只需要找三次方项即可,时间复杂度最坏是 \(\mathcal{O}(\sqrt{N} \times \ln N)\),但很显然远远跑不满,可以获得 60 pts 的好成绩。
容易发现瓶颈在于筛质数,于是考虑少筛一点。
具体的,我们考虑筛出不超过 \(N^{1/4}\) 的质数。
现在我们就只需要考虑 \(>N^{1/4}\) 的三次方质因子了。
不妨猜想这样的质因子只有一个,事实上这个结论是正确的。
考虑反证法,假设这样的质因子有两个,我们分别令其为 \(x_1,x_2\)。
由于 \(x_1,x_2 > N^{1/4}\),所以有 \(x_1^3x_2^3 > ((N^{1/4})^2)^3=N^{3/2} > N\),这显然不合法,命题得证。
接下来,我们令筛去 \(N^{1/4}\) 以内的质因子后的 \(N\) 为 \(N'\),那个唯一的质因子为 \(x\)。
显然,有 \(N'=x^3 \times p\),其中 \(p\) 为筛出来的三次方质因子的乘积。
又因为 \(p\) 必须是 \(>N^{1/4}\) 的(因为小于等于的已经算过了),所以两部分都大于 \(N^{1/4}\),又不合法了,于是 \(p\) 只能等于 \(1\)。
所以,本题的做法呼之欲出:先筛出 \(N^{1/4}\) 以内的质数,乘到答案里,然后判断最后的 \(N'\) 是否为完全立方数,是的话就乘上那个质因子即可。
需要注意的是,判断是否为完全立方数的部分最好用二分,cbrt 会炸。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e4;
const int M=1e5+5;
int _,tot;
double n;
bool vis[M];
int pr[M];
void E(){
vis[0]=vis[1]=1;
for(int i=2;i<=N;i++){
if(!vis[i]){
pr[++tot]=i;
for(int j=i+i;j<=N;j+=i)
vis[j]=1;
}
}
}
void solve(){
cin>>n;
int ans=1;
for(int i=1;i<=tot;i++){
int cnt=0;
while(n%pr[i]==0){
n/=pr[i],cnt++;
if(cnt==3)
ans*=pr[i],cnt=0;
}
}
n=(int)n;
if(cbrt(n)==(int)cbrt(n))
ans*=cbrt(n);
cout<<ans<<'\n';
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
//freopen("T2.in","r",stdin);
//freopen("T2.out","w",stdout);
cin>>_,E();
while(_--)
solve();
return 0;
}
总结:
-
乘积式考虑质因数分解。
-
复杂度过高时考虑分段处理(根号分治思想)。
C
预估 30,实际 30。
每次 BFS 跑最短路,可以获得 30 pts 的好成绩。
考虑弱化版问题。如果图退化为一棵树,直接 树上差分 即可。
观察到边很少,最多 \(n+200\),这意味着最多比树多 \(200\) 个点,于是可以对这 \(200\) 个点跑 BFS 然后取 \(\min\) 即可。时间复杂度单 \(\log\) 加上 \(200n\) 的常数。
注意这个方法对于前 \(30\%\) 的数据并不适用,所以需要把两部分拼起来。实现时可以使用 namespace 封装。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,M=2e2+5,K=48;
int n,m,t,cnt;
int Dis[N],dis[M][N],fa[N],dep[N],dp[N][K];
bool vis[N];
vector<int> G[N];
namespace sol1{
void bfs(int s){
queue<int> q;
q.push(s);
memset(Dis,0x3f,sizeof Dis);
Dis[s]=0;
memset(vis,0,sizeof vis);
while(!q.empty()){
int cur=q.front();
q.pop();
if(vis[cur])
continue;
vis[cur]=1;
for(int i:G[cur]){
if(Dis[i]>Dis[cur]+1){
Dis[i]=Dis[cur]+1;
q.push(i);
}
}
}
}
void solve(){
cin>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
cin>>t;
while(t--){
int a,b;
cin>>a>>b;
bfs(a);
cout<<Dis[b]<<'\n';
}
}
}
namespace sol2{
void bfs(int s){
++cnt;
queue<int> q;
q.push(s);
dis[cnt][s]=0;
memset(vis,0,sizeof vis);
while(!q.empty()){
int cur=q.front();
q.pop();
if(vis[cur])
continue;
vis[cur]=1;
for(int i:G[cur]){
if(dis[cnt][i]>dis[cnt][cur]+1){
dis[cnt][i]=dis[cnt][cur]+1;
q.push(i);
}
}
}
}
void initLCA(int cur,int fa){
dep[cur]=dep[fa]+1;
dp[cur][0]=fa;
for(int i=1;(1<<i)<=dep[cur];i++)
dp[cur][i]=dp[dp[cur][i-1]][i-1];
for(int i:G[cur])
if(i!=fa)
initLCA(i,cur);
}
int queryLCA(int x,int y){
if(dep[y]>dep[x]) swap(x,y);
for(int i=20;i>=0;i--)
if(dep[dp[x][i]]>=dep[y])
x=dp[x][i];
if(x==y)
return x;
for(int i=20;i>=0;i--)
if(dp[x][i]!=dp[y][i])
x=dp[x][i],y=dp[y][i];
return dp[x][0];
}
int fnd(int x){
return x==fa[x]?x:fa[x]=fnd(fa[x]);
}
void solve(){
cin>>m;
vector<pair<int,int> > vec;
for(int i=1;i<=n;i++)
fa[i]=i;
memset(dis,0x3f,sizeof dis);
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
int uu=fnd(u),vv=fnd(v);
if(uu!=vv){
G[u].push_back(v);
G[v].push_back(u);
fa[uu]=vv;
}
else
vec.push_back({u,v});
}
initLCA(1,0);
for(auto i:vec){
int u=i.first,v=i.second;
G[u].push_back(v);
G[v].push_back(u);
}
for(auto i:vec){
int u=i.first;
bfs(u);
}
cin>>t;
while(t--){
int a,b;
cin>>a>>b;
int lca=queryLCA(a,b);
//cout<<lca<<'\n';
int ans=dep[a]+dep[b]-2*dep[queryLCA(a,b)];
for(int i=1;i<=cnt;i++)
ans=min(ans,dis[i][a]+dis[i][b]);
cout<<ans<<'\n';
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
//freopen("T3.in","r",stdin);
//freopen("T3.out","w",stdout);
cin>>n;
if(n<=1000)
sol1::solve();
else
sol2::solve();
return 0;
}
总结:
-
关注数据范围。
-
考虑弱化版问题。
D
预估 10,实际 10。
观察到 \(n \le 20\) 考虑 状压 dp。
容易发现最大前缀有很多重复的机会,于是考虑统计最大前缀的方案数。
既然是计数,就需要一些约束条件。经过分析,可以发现下列性质(令最大前缀和断点为 \(x\)):
-
\(\forall i \in [1,x],\sum^x_{j=i} a_j \ge 0\)(不然就可以删掉一些前缀使得答案更大)
-
\(\forall i \in [x+1,n],\sum ^{i}_{j=x+1} a_j < 0\)(不然就可以添加一些后缀使得答案更大)
于是,令 \(dp1_i\) 表示二进制状态 \(i\) 下满足性质 \(1\) 的前缀方案数,\(dp2_i\) 表示满足性质 \(2\) 的后缀方案数,显然这两种状态是互补的,并且可以递推预处理。
令 \(sum_i\) 表示二进制状态 \(i\) 下的 \(\sum a_i\),则答案即为 \(\sum dp1_i \times dp2_{(2^n-1) \oplus i} \times sum_i\)。
时间复杂度 \(\mathcal{O}(n2^n)\)。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=21;
const int MOD=998244353;
int n;
int a[N],sum[1<<N],dp1[1<<N],dp2[1<<N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
//freopen("T4.in","r",stdin);
//freopen("T4.out","w",stdout);
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
for(int i=0;i<(1<<n);i++)
for(int j=0;j<n;j++)
if(((i>>j)&1))
sum[i]=(sum[i]+a[j])%MOD;
dp1[0]=dp2[0]=1;
for(int i=1;i<(1<<n);i++){
for(int j=0;j<n;j++){
if((i>>j)&1){
if(sum[i]<0)
dp2[i]=(dp2[i]+dp2[i^(1<<j)])%MOD;
if(sum[i^(1<<j)]>=0)
dp1[i]=(dp1[i]+dp1[i^(1<<j)])%MOD;
}
}
}
int all=(1<<n)-1,ans=0;
for(int i=0;i<(1<<n);i++)
ans=(ans+((dp1[i]%MOD*dp2[all^i]%MOD)%MOD*sum[i]%MOD)%MOD)%MOD;
cout<<(ans%MOD+MOD)%MOD;
return 0;
}
总结:
-
关注数据范围。
-
关注样例。
结语
成绩:60+30+30+10=130,并未挂分。
问题:背包模型不熟练,对数论问题不够敏感,题目性质挖掘不够深入。
方案:
-
加强背包模型、质因数分解技巧的掌握程度。
-
对于数据范围、样例多进行观察和模拟,挖掘性质。
以上。

浙公网安备 33010602011771号