CF 1900~2000 DP

开个新坑。

CF 1900~2000 DP

迫真可视化

  • \(\color{Gold}{★}\) 表示未参考题解,\(\color{Red}{★}\) 反之。
  • \(\color{Gold}{⊚},\color{Red}{⊚},\color{Blue}{⊚}\) 表示此题在个人看来的价值。

CF1408D Searchlights

CF1408D Searchlights

\(\color{Red}{★} \ \color{Red}{⊚}\)

很该死,思路一直都在二维的状态,出不来。
唔,主要是不能看到数据范围不卡 \(O(nm)\) 就一股脑把所有都往二维上堆,会变得不幸。

对于一个人,设向右走 \(x\) 步,再向上走 \(y\) 步。
这时可以暴力枚举 \(x\) 的值,然后预处理出所有人在向右走 \(i\) 步时,要向上走的最小步数 \(f_i\),就能 \(O(x)\) 的更新答案了。

#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
const int N=2010,INF=1e9;
int n,m,ma,f[N*N],ans,cnt;
pi a[N],b[N];
void work(){
  cin>>n>>m;ans=INF;
  L(i,1,n) cin>>F(a[i])>>S(a[i]);
  L(i,1,m) cin>>F(b[i])>>S(b[i]);
  L(i,1,n) L(j,1,m) ma=max(ma,S(b[j])-S(a[i])+1);
  L(i,1,n) L(j,1,m) if(S(b[j])>=S(a[i])) 
    f[S(b[j])-S(a[i])]=max(f[S(b[j])-S(a[i])],F(b[j])-F(a[i])+1);
  R(i,ma,0) f[i]=max(f[i],f[i+1]);
  L(i,0,ma) ans=min(ans,i+f[i]);cout<<ans;
}

CF1268B Searchlights

CF1268B Searchlights

\(\color{Gold}{★} \ \color{Blue}{⊚}\)

所以这题为啥会有 DP 的标签?

就对整个图表黑白染色,输出两种颜色中块数的较小值。

证明:

  • 必要性:每个骨牌必然只能匹配一个黑格和一个白格。

  • 充分性:假设有一个黑格和一个白格未匹配,则必然可以通过调整当前方案来使得它们都处于匹配状态。

考虑选择一对黑格和白格,然后选择一条它们之间的路径,这条路径满足除起点和终点之外都是已匹配的格子,那么对这条路径进行调整就可以得到满足条件的解。

数组不能直接开,就模拟一下就行了。

时间复杂度:\(O(\max(n,a_i))\)

#define vi vector<int>
#define all(x) (x).begin(),(x).end()
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
const int N=3e5+10,m1=1e9+7,m2=998244353,INF=1e18;
void work(){
  int n;cin>>n;vi a(n),s(N),f(N);
  fill(all(s),n);int cnt=0,c1=0,c2=0;
  for(int &i:a) cin>>i,f[i]++;
  L(i,1,N-10) s[i]-=cnt,cnt+=f[i];
  L(i,1,N-10) 
    if(i&1) c1+=s[i]/2,c2+=s[i]/2+(s[i]&1);
    else c2+=s[i]/2,c1+=s[i]/2+(s[i]&1);
  cout<<min(c1,c2);
}

CF1257E The Contest

CF1257E The Contest

\(\color{Gold}{★} \ \color{Blue}{⊚}\)

妈的这种神比题也配评分 2000 ?!
\(f_{i,j}\) 表示目前枚举到第 \(i\) 道题,当前以第 \(j\) 人结束的最小时间。
那就 \(f_{i,j}\) 可以从 \(f_{i-1,k} \ (k \le j)\) 转移过来,再加上本来 \(j\) 人是否被分配到题目 \(i\) 就行了。
本人实在是懒,就直接用 STL 爆艹了。
时间复杂度:\(O(n \log_2 n)\),不过当然可以做到 \(O(n)\)

#define vi vector<int>
#define id(x,y) ((x-1)*m+y)
#define all(x) (x).begin(),(x).end()
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
int f[3][N];
void work(){
  int k1,k2,k3;cin>>k1>>k2>>k3;
  vi a(k1),b(k2),c(k3);int n=k1+k2+k3;
  for(int &i:a) cin>>i;sort(all(a));
  for(int &i:b) cin>>i;sort(all(b));
  for(int &i:c) cin>>i;sort(all(c));
  L(i,1,n) L(j,0,2) f[j][i]=INF;
  L(i,1,n){
    f[0][i]=f[0][i-1]+(!binary_search(all(a),i));
    f[1][i]=min(f[1][i-1]+(!binary_search(all(b),i)),f[0][i-1]+(!binary_search(all(b),i)));
    f[2][i]=min({f[2][i-1],f[1][i-1],f[0][i-1]})+(!binary_search(all(c),i));
  }cout<<min({f[0][n],f[1][n],f[2][n]});
}

CF1612E Messages]

CF1612E Messages

\(\color{Red}{★} \ \color{Gold}{⊚}\)

对于选出的 \(t\) 个数 \(c_{1},\cdots,c_{t}\),贡献值为 $$\sum_{i=1}^n \left[ \exists j,m_i=c_j \right] \dfrac{\min{i_i,t}}{t}$$

完全可以开个桶保存,取前 \(t\) 大的贡献值即可。

试证明 \(t \le 20\) 时,保证可以取得答案。

证明如下:
因为题目保证 \(k_i \le 20\) 所以取 \(21\) 个数时,所有数的贡献都转变为 \(k_i\)
我们设 \(f_{20}\) 是取 \(20\) 个数时的最优解,那么在取 \(21\) 个数时,前面取的数明显还是不变,那么贡献就会变成 \(\dfrac{20}{21} f_{20}\),但很明显,每次取的数的贡献是单调不升的。
所以此时取 \(21\) 个数,贡献值显然无法大于 \(\dfrac{1}{21} f_{20}\)
Q.E.D

#define vi vector<int>
#define pi pair<int,int>
#define F(i) (i).first
#define S(i) (i).second
#define all(x) ((x).begin(),(x).end())
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
vi ans;double ma;
pi a[N],f[N];
int b[N][22];
void work(){
  int n;cin>>n;
  L(i,1,n) cin>>F(a[i])>>S(a[i]);
  L(i,1,20){
    double s=0.0;
    L(j,1,n) b[F(a[j])][i]+=min(S(a[j]),i);
    L(j,1,N-10) f[j]={b[j][i],j};
    nth_element(f+1,f+i,f+N-10,greater<pi>());
    L(j,1,i) s+=F(f[j]);s/=i;
    if(s>ma){
      ma=s;ans.clear();
      L(j,1,i) ans.pb(S(f[j]));
    }
  }cout<<ans.size()<<'\n';
  for(int i:ans) cout<<i<<' ';
}

CF864E Fire

CF864E Fire

\(\color{Gold}{★} \ \color{Red}{⊚}\)

很显然的背包。
对于每个物品有救或者不救,完全符合背包。
加入了 \(d_i\) 这个新参数,为了维持 DP 的无效性,很容易想到对所有物品以 \(d_i\) 为关键字排序。
之后就是 DP 了,因为要输出方案,就开 \(2000\)std::vector<int> 直接存储,转移时更新即可。
最后就是一定要注意,第 \(0\) 秒时无法保存物品,DP 边界值要改一下。本人因为这个 WA 了两发

时间复杂度:\(O(n\cdot \sum_{i=1}^n d_i)\)

#define pb push_back
#define vi vector<int>
#define all(x) (x).begin(),(x).end()
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
#define R(i,j,k) for(int (i)=(j);i>=(k);(i)--)
const int N=110;
struct node{int t,d,p,id;}a[N];
bool cmp(node a,node b){return a.d<b.d;}
void work(){
  int n,c=0,x=0;cin>>n;
  vi f(2010);fill(all(f),0);
  vector<vi>ans(2010);
  L(i,1,n) cin>>a[i].t>>a[i].d>>a[i].p;
  L(i,1,n) a[i].id=i;sort(a+1,a+n+1,cmp);
  L(i,1,n){
    if(a[i].d<=a[i].t) continue;
    R(j,a[i].d,a[i].t+1){
      if(f[j-a[i].t]+a[i].p>f[j]){
        f[j]=f[j-a[i].t]+a[i].p;
        ans[j]=ans[j-a[i].t];ans[j].pb(a[i].id);
      }
    }
  }R(i,2000,0) if(f[i]>x) x=f[i],c=i;
  cout<<x<<'\n'<<ans[c].size()<<'\n';
  for(int i:ans[c]) cout<<i<<' ';
}

CF1616D Keep the Average High

CF1616D Keep the Average High

\(\color{Gold}{★} \ \color{Red}{⊚}\)

我的做法和所有题解都不一样,而且蠢到了极致

对于每个数,如果不满足条件就去掉,清空后继续枚举之后的数。
因为某个数删去之后,这个数前面和后面的数共同组成的子串显然就满足条件。

所以问题转化为:如何判断某个数 \(a_i\) 是否能满足 \(\forall j < i,\sum\limits_{k=j}^i a_k \le (i-k+1) \times x\)

可以想到维护某个数之前类似于最大后缀和的一个数,就是把整个数组减去 \(x\) 之后的最大后缀和加上长度乘以 \(x\) 的值。
显然,如果这个数与 \(a_i\) 之和满足条件,那么 \(a_i\) 就满足条件。

时间复杂度:\(O(n)\)(但比题解烂多了)

#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
const int N=5e4+10,INF=1e18;
int n,x,a[N];
void work(){
  cin>>n;L(i,1,n) cin>>a[i];cin>>x;
  int t=0,z=INF,ans=n,l=0;
  L(i,1,n){
    l++;
    if(a[i]+z<(t+1)*x&&l!=1){
      ans--;
      l=0;z=INF;
      continue;
    }t++;
    if(z<(t-1)*x) z+=a[i];
    else z=a[i],t=1;
  }cout<<ans<<'\n';
}

CF1101D GCD Counting

CF1101D GCD Counting

\(\color{Red}{★} \ \color{Gold}{⊚}\)

同样是 CF 2000 分的 DP,有的题在 Luogu 上是黄题,有的却是紫题。

树形 DP + 分解质因数。

首先,我们完全可以把数组 \(a\) 中每个值的质因数在 \(O(n \sqrt{n})\) 的时间内求出并存储。
因为每个数的不同的质因数不可能超过 \(10\) 个(把最小的那些质因数累乘起来就能知道),所以不会爆空间,而且我们不关注数量。

然后是 DP 环节。

\(f_{u,i}\) 表示:以 \(u\) 作为链的 \(\text{LCA}\) 且链的 \(\gcd\)\(i\) 的倍数时的最优解。

这样,转移方程就是:$$f_{u,i} = \max\limits_{v \in son_u,fac_{u,i}=fac_{v,j}} f_{v,j}+1$$

答案就是 $$\max\limits_{v,w \in son_u,fac_{v,i}=fac_{w,j}} f_{v,i}+f_{w,j}+1$$
其实不用再跑一遍 dfs 统计答案,完全可以在 DP 的转移的过程中完成更新,具体看代码。
时间复杂度:\(O(n \sqrt{n})\)(瓶颈在分解质因数)

#define pb push_back
#define vi vector<int>
#define pi pair<int,int>
#define all(x) (x).begin(),(x).end()
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
#define ll(i,j,k,l) for(int (i)=(j);i<=(k);(i)+=(l))
const int N=5e5+10;
vi g[N],zs[N],z;bitset<N>p;
int n,a[N],f[N][20],ans,ff;
void pre(){
  p.set();p[0]=p[1]=0;
  L(i,2,(sqrt(N-10))) if(p[i])
    ll(j,(i<<1),N-10,i) p[j]=0;
  L(i,2,N-10) if(p[i]) z.pb(i);
}vi fj(int x){
  set<int>s;s.clear();
  int g=sqrt(x);
  for(int i:z){
    if(i>g) break;
    while(!(x%i)) x/=i,s.insert(i);
  }if(x>1) s.insert(x);
  vi ans(all(s));return ans;
}void dfs(int u,int fa){
  for(int v:g[u]){
    if(v==fa) continue;
    dfs(v,u);int x=1,y=1;
    L(i,0,zs[u].size()-1)
      L(j,0,zs[v].size()-1)
        if(zs[u][i]^zs[v][j]){
          ans=max(ans,f[u][x]+f[v][y]+1);
          f[u][x]=max(f[u][x],f[v][y]+1);
        }
  }
}void work(){
  cin>>n;L(i,1,n) cin>>a[i];
  L(i,1,n) if(a[i]>1) ff=1;
  L(i,1,n-1){
    int u,v;cin>>u>>v;
    g[u].pb(v);g[v].pb(u);
  }L(i,1,n) if(a[i]^1) zs[i]=fj(a[i]);
  dfs(1,0);cout<<ans+ff;
}

CF895C Square Subsets

CF895C Square Subsets

\(\color{Red}{★} \ \color{Gold}{⊚}\)

好!又是一道紫题!又把 \(\color{Gray}{A.I.skeleton}\) 彻底干爆了!

显然,对于一个数是否为完全平方数,只和其质因数数量有关。
\(70\) 小的质数只有 \(19\) 个,可以考虑状压。
然后 \(n \le 10^5\) 就完全失去了意义,因为完全可以开一个桶,\(t_i\) 表示数字 \(i\) 出现次数。

考虑 DP 状态设计。
\(f_{i,s}\) 表示当前考虑到所有 \(\le i\) 的数,且当前质因数的奇偶状态为 \(s\) 时的方案数。

  • 如果选择偶数个 \(i\)\(s\) 不会改变,\(f_{i,s}\) 可以从 \(f_{i-1,s}\) 直接转移过来。
  • 如果选择奇数个 \(i\),那就对 \(i\) 分解质因数,对 \(s\) 取异或得到 \(p\),然后 \(f_{i,p}\) 就能从 \(f_{i-1,s}\) 转移。

设当前 \(t_i=k\),那么选奇数个情况的方案数就是:\(\sum\limits_{i \le k,i \equiv 1 \pmod{2}}^k \dbinom{i}{k}= 2^{k-1}\),选偶数个情况的方案数就是:\(\sum\limits_{i \le k,i \equiv 0 \pmod{2}}^k \dbinom{i}{k}= 2^{k-1}\)

上面的式子可以用二项式定理证明。

而且,\(f_{i,s}\) 只会由 \(f_{i-1}\) 转移而来,那就能滚动一下数组。

最后,状态转移方程式就是:\(\begin{cases} f_{i,s} = f_{i-1,s} \times 2^{k-1} \\ f_{i,p} = f_{i-1,s} \times 2^{k-1} \end{cases}\)

如果被卡了:

  • 不用快速幂,先 \(O(n)\) 预处理幂表。
  • 不要在每一种状态的循环中重新处理 \(p\),对于每个 \(i\) 先分解质因数再与当前状态 \(s\) 取异或即可得到 \(p\)
  • 可以不开 long long,就是乘的部分加上 1ll* 就行。

时间复杂度: \(O(\max a_i \cdot 2^{19})\)

#define vi vector<int>
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
const int N=2e5+10,m1=1e9+7;
vi pr={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67};
int t[90];vi a;int f[(1<<19)+1],g[(1<<19)+1],po[N];
void work(){
  int n;cin>>n;a.resize(n);
  po[0]=1;L(i,1,n) po[i]=(po[i-1]<<1)%m1;
  for(int &i:a) cin>>i,t[i]++;g[0]=1;
  L(i,1,70){
    if(!t[i]) continue;
    int p=0,x=i;L(j,0,18) 
      while(x&&(!(x%pr[j])))
        x/=pr[j],p^=(1<<j);
    L(s,0,((1<<19)-1)){
      int P=s^p;
      (f[s]+=1ll*g[s]*po[t[i]-1]%m1)%=m1;
      (f[P]+=1ll*g[s]*po[t[i]-1]%m1)%=m1;
    }L(j,0,((1<<19)-1)) g[j]=f[j],f[j]=0;
  }cout<<(g[0]-1+m1)%m1;
}

貌似还有更为 NB 的线性基维护的做法,但是窝太菜了,不会。\kk

CF710E Generate a String

CF710E Generate a String

\(\color{Gold}{★} \ \color{Red}{⊚}\)

首先,\(n \le 10^7\),所以正解多半是 \(O(n)\) 的,\(O(n \log_2 n)\) 的暴力 DP 做法就变的不是那么可取。
考虑 \(f_i\) 表示数字 \(i\) 的最小花费。

根据题目的定义有如下的转移: \(f(x)=\begin{cases} \min \left\{f_{i-1}+x,f_{i+1}+x \right\} &x \bmod 2 = 1\\ \min \left\{f_{i-1}+x,f_{i+1}+x,f_{i/2}+y \right\} & x \bmod 2 = 0 \end{cases}\)

但很明显,这样的转移必超时,因为有往回重新更新的过程。
所以要替换掉 \(f_{i+1}+x\) 这一项。

注意到当 \(i\) 的奇偶性不同时,\(f_i\) 的转移状况会有所不同,所以我们可以考虑分 \(i\) 为奇偶的情况讨论。

  • \(i\) 为偶数时:

\(f_{i}\) 如果从 \(f_{i+1}\) 转移,说明 \(f_{i+1}\) 只能由 \(f_{i+2}\) 转移,然后 \(f_{i+2}\) 只能由 \(f_{(i+2)/2}\) 转移,代价是 \(y + 2 \cdot x\),显然不如 \(f_{(i+2)/2}\)\(f_{i/2}\) 再到 \(f_{i}\) 代价为 \(x\) 转移更优。
所以 \(f_i\) 此时不用考虑从 \(f_{i+1}\) 转移的情况了。

  • \(i\) 为奇数时:

\(f_i\) 不能从 \(f_{i/2}\) 转移,但是可以从 \(f_{(i+1)/2}\) 转移,此时代价为 \(x+y\)
另一种情况是 \(f_{i+1}\) 再到 \(f_{i+2}\),因为 \(i+2\) 是奇数,所以又回到之前 \(i\) 为偶数时的讨论情况,则可知代价为 \(y+4 \cdot x\),显然可以舍去这种情况。

整理得到最后的状态转移方程:\(f(x)=\begin{cases} \min \left\{f_{i-1}+x,f_{(i+1)/2}+x+y \right\} &x \bmod 2 = 1\\ \min \left\{f_{i-1}+x,f_{i/2}+y \right\} & x \bmod 2 = 0 \end{cases}\)

时间复杂度:\(O(n)\)

#define int long long
#define vi vector<int>
#define all(x) (x).begin(),(x).end()
#define L(i,j,k) for(int (i)=(j);(i)<=(k);(i)++)
const int N=1e7+10,INF=1e18;
int n,x,y;vi f(N);
void work(){
  cin>>n>>x>>y;fill(all(f),INF);
  f[0]=0;L(i,1,n)
    f[i]=min(f[i-1]+x,((i&1)?f[(i+1)/2]+x+y:f[i/2]+y));
  cout<<f[n];
}

CF1294F

CF1294E Generate a String

\(\color{Gold}{★} \ \color{Blue}{⊚}\)

这是 DP?(好吧洛谷题解里确实有 DP 做法)

反正就先考虑直径,显然直径的两个端点是都要选的。
证明不会,但考虑到更长的链贡献会更大,所以直径应该还是很好想的。
然后去掉直径上所有点的贡献,再从直径上随便一个点跑出当前最优的点就是所求的第三个点。
链的情况就把第三个点的初始值赋为链上的非端点的一个点就行了,如果不是链就会被更新掉。

时间复杂度:\(O(n)\)

#define si(i) ((int)i.size())
#define pb push_back
#define me(t,x) memset(t,x,sizeof(t))
#define L(i,j,k) for(int (i)=(j);i<=(k);(i)++)
void dfs1(int u,int fa){
  for(int v:g[u]) if(v^fa)
    d[v]=d[u]+(!vis[v]),dfs1(v,u);
}void dfs3(int u){
  for(const int &v:g[u]) if(v^fa[u]) fa[v]=u,dfs3(v);
}void work(){
  int ma=0,x,y,z;
  cin>>n;L(i,1,n-1) 
    cin>>u>>v,g[u].pb(v),g[v].pb(u);
  L(i,1,n) vis[i]=1;dfs1(1,1);
  L(i,1,n) if(d[i]>ma) 
    ma=d[i],x=i;
  me(d,0);dfs1(x,x);ma=0;
  L(i,1,n) if(d[i]>ma) ma=d[i],y=i;
  dfs3(y);for(int u=x;u;u=fa[u]) st.pb(u),vis[u]=0;
  L(i,1,n) if((i^x)&&(i^y)){z=i;break;}
  me(d,0);dfs1(x,x);ma=0;
  L(i,1,n) if(d[i]>ma) ma=d[i],z=i;
  cout<<si(st)-1+ma<<'\n'<<x<<' '<<y<<' '<<z;
}

CF1296E2

CF1296E2 Generate a String

\(\color{Gold}{★} \ \color{Red}{★} \ \color{Gold}{⊚}\)

关于某人口胡出了题目的全部思路但是不会写板子导致暴毙而去看题解这回事
思维题,通过人类大脑的工作可知,答案就是最长下降子序列的长度,每个点的颜色就是 DP 过程中的 \(f_i\)

证明我不会\kk

如果是这道题,因为只有 \(26\) 种不同的数,完全可以枚举过去。
时间复杂度:\(O(26 \cdot n)\)

int n,f[N],ans,g[30];char s[N];
void work(){
  cin>>n>>(s+1);R(i,n,1){
    f[i]=g[ch(s[i])]+1;
    L(j,ch(s[i])+1,25) g[j]=max(g[j],f[i]);
  }L(i,1,n) ans=max(ans,f[i]);
  cout<<ans<<'\n';L(i,1,n) cout<<f[i]<<' ';
}

那就借此机会复习一下最长下降子序列把。(话说我导弹拦截至今没写诶)

先考虑 \(O(n^2)\) 的 DP:
\(f_i\) 表示从第 \(i\) 个数开始的最长下降子序列的长度。
最终答案就是 \(\max \left\{ f_i \right\} \ (i \le n)\)

转移为:如果有 \(i < j \text{且} a_i < a_j,f_j = \max \left\{ f_i + 1 \right\}\)

那么代码也就不难写出了。

int n,ans,a[N];
void work(){
  cin>>n;vi f(n);fill(all(f),1);
  L(i,0,n-1) cin>>a[i];
  L(i,0,n-1) L(j,0,i-1) 
    if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
  L(i,0,n-1) ans=max(ans,f[i]);cout<<ans;
}

但如果只会 \(O(n^2)\) 的做法的话,就会连上一题都过不去。
所以我们需要 \(O(n \log_2 n)\) 的更为优秀的做法。

主要思路就是 贪心+二分
害,具体不太想写了,具体看下代码把。

int a[N],b[N]={INF},ans;
int lds(){
  L(i,1,n) cin>>a[i];
  L(i,1,n){
    int l=1,r=ans;
    while(l<=r){
      int mid=(l+r)>>1;
      if(b[mid]>a[i]) l=mid+1;
      else r=mid-1;
    }b[l]=a[i];ans+=(l>ans);
  }cout<<ans;
}

THE END

不出意外的话这应该是最后一题了,之后估计要去刷别的类型题。
希望能有继续更的那一天。

posted @ 2022-09-13 19:56  AIskeleton  阅读(31)  评论(0编辑  收藏  举报