dp 练习记录...
streduc
(自己做出)
因为字符串拼接,从左到右考虑不太行。
把所有\(|S|\)集合都插入trie内
考虑把字符串消除过的点的连续段拿出来。
设\(f_{l,r,p}\)表示原串\([l,r]\)区间,外部恰好有一个串,走到trie的\(p\)节点,是否可行。\(g_{l,r}\)表示\([l,r]\)是否能够消完。
转移:\(f_{l,r+1,nxt}|=f_{l,r,p}\),nxt表示\(x\)走\(s_{r+1}\)后的节点。
如果\([r+1,t]\)能够被全部消完(\(g_{r+1,t}=1\)),则\(f_{l,t,p}|=f_{l,r,p}\)
如果存在一个\(p\),使得\(p\)是终止节点且\(f_{l,r,p}=1\)则\(g_{l,r}=1\)。
求出答案可以另外用一个dp。
设\(h_i\)表示消除\([1,i]\)需要保留多少个字符。转移有\(h_i=\min(h_i,h_{i-1}+1)\)。
如果\(g_{j,i}=1\),\(h_i=\min(h_i,h_{j-1})\)。
发现数据范围太大,所以要把\(f_{l,r}\)视为一个bitset。
#include<bits/stdc++.h>
using namespace std;
#define N 260
char c[N],cc[N];
int n,ch[N*10][26],ct,bz[N*10],h[N],g[N][N],le;
bitset<N*3>f[N][N];
void ins(char *s){
int x=0;
for(int i=1;s[i];i++){
if(!ch[x][s[i]-'a']){
ch[x][s[i]-'a']=++ct;
}
x=ch[x][s[i]-'a'];
}
bz[x]=1;
}
int main(){
scanf("%s%d",c+1,&n);
le=strlen(c+1);
for(int i=1;i<=n;i++){
scanf("%s",cc+1);
ins(cc);
}
memset(h,127,sizeof(h));
for(int i=1;i<=le;i++)
f[i][i-1][0]=1;
for(int l=0;l<=le;l++)
for(int i=1;i<=le;i++)
if(i+l-1<=le){
int r=i+l-1;
for(int j=i;j<r;j++)
if(g[j+1][r])
f[i][r]|=f[i][j];
for(int k=0;k<=ct;k++)
if(f[i][r][k]&&r+1<=le&&ch[k][c[r+1]-'a'])
f[i][r+1][ch[k][c[r+1]-'a']]=1;
for(int k=0;k<=ct;k++)
if(bz[k]&&f[i][r][k])
g[i][r]=1;
}
h[0]=0;
for(int i=1;i<=le;i++){
h[i]=h[i-1]+1;
for(int j=1;j<=i;j++)
if(g[j][i])
h[i]=min(h[i],h[j-1]);
}
printf("%d",h[le]);
}
Bookshelf
(自己做出)
简单题
考虑dp,设\(f_i\)表示摆放前\(i\)个,\(s\)是前缀和数组。
则有转移方程:\(f_i=\min(f_{j-1}+\max(h_j...h_i)),s_i-s_j\leq l\)
满足条件的\(j\)是一段连续区间可以二分。
移动\(i\)后,用线段树维护\(j\)下标处\(f_{j-1}+\max(h_j...h_i)\)的最大值。
显然建立单调栈后区间加法维护。
#include<bits/stdc++.h>
using namespace std;
#define N 1000010
#define int long long
int n,l,h[N],w[N],tg[N],mx[N],s[N],f[N],st[N],tp;
void pd(int o){
if(tg[o]){
tg[o*2]+=tg[o];
tg[o*2+1]+=tg[o];
mx[o*2]+=tg[o];
mx[o*2+1]+=tg[o];
tg[o]=0;
}
}
void mod(int o,int l,int r,int x,int y){
if(l==r){
mx[o]=y;
return;
}
int md=(l+r)/2;
pd(o);
if(x<=md)
mod(o*2,l,md,x,y);
else
mod(o*2+1,md+1,r,x,y);
mx[o]=min(mx[o*2],mx[o*2+1]);
}
void ad(int o,int l,int r,int x,int y,int z){
if(r<x||y<l)
return;
if(x<=l&&r<=y){
tg[o]+=z;
mx[o]+=z;
return;
}
pd(o);
int md=(l+r)/2;
ad(o*2,l,md,x,y,z);
ad(o*2+1,md+1,r,x,y,z);
mx[o]=min(mx[o*2],mx[o*2+1]);
}
int qu(int o,int l,int r,int x,int y){
if(r<x||y<l)
return 1e18;
if(x<=l&&r<=y)
return mx[o];
int md=(l+r)/2;
pd(o);
return min(qu(o*2,l,md,x,y),qu(o*2+1,md+1,r,x,y));
}
int gt(int x,int po){
int l=0,r=po,ans=0;
while(l<=r){
int md=(l+r)/2;
if(s[md]>=x){
ans=md;
r=md-1;
}
else
l=md+1;
}
return ans;
}
signed main(){
scanf("%lld%lld",&n,&l);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&h[i],&w[i]);
s[i]=s[i-1]+w[i];
}
memset(mx,32,sizeof(mx));
mod(1,1,n,1,h[1]);
for(int i=1;i<=n;i++){
while(tp&&h[i]>=h[st[tp]]){
ad(1,1,n,st[tp-1]+1,st[tp],h[i]-h[st[tp]]);
tp--;
}
st[++tp]=i;
int po=gt(s[i]-l,i);
f[i]=qu(1,1,n,po+1,i);
mod(1,1,n,i+1,f[i]+h[i+1]);
}
printf("%lld",f[n]);
}
[HNOI2007]梦幻岛宝珠
(瞄了一眼题解)
数据范围很奇怪,自己的做法也有点奇怪
把所有数按照\(b\)从大到小排序后dp。
设\(f_i\)表示前\(i\)个数的答案。
然而这还不够,因为我们要知道背包的容量。
设\(f_{i,j}\)表示前\(i\)个数,背包容量除以\(2^i\)等于\(j\)的答案。
转移先把\(f_{i,j}\)转移到\(f_{i-1,j*2+p}\),其中\(p\)是\(w\)的二进制表示在\(i\)位是否是\(1\)。
然后考虑添加当前重量为\(s\)的物品,\(f_{i,j-s}\)转移到\(f_{i,j}\)。
发现我们要做的是01背包,不是完全背包。
所以要正着枚举\(j\)。
然而\(j\)太大。
由于\(a\)很小,所以每次把\(j\)和\(1000\)取最小值即可。
我发现这种做法在lg题解区只有一个人用过
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 110
int n,w,a[N],b[N],va[N],v[N],f[32][1010];
vector<int>g[N];
signed main(){
while(scanf("%lld%lld",&n,&w)==2){
if(n==-1)
break;
memset(f,-63,sizeof(f));
memset(b,0,sizeof(b));
memset(a,0,sizeof(a));
for(int i=0;i<=30;i++)
g[i].clear();
for(int i=1;i<=n;i++){
scanf("%lld%lld",&va[i],&v[i]);
int x=va[i];
while(x%2==0){
b[i]++;
x/=2;
}
a[i]=x;
g[b[i]].push_back(i);
}
for(int i=30;~i;i--){
int p=((w&(1ll<<i))>0),p1=((w&(1ll<<(i-1)))>0);
f[i][p]=max(f[i][p],0ll);
for(int j=0;j<g[i].size();j++){
int x=g[i][j];
for(int k=a[x];k<=1000;k++)
f[i][k-a[x]]=max(f[i][k-a[x]],f[i][k]+v[x]);
}
if(i){
for(int j=0;j<=1000;j++)
f[i-1][min(1000ll,p1+j*2)]=max(f[i-1][min(1000ll,p1+j*2)],f[i][j]);
}
}
int ans=0;
for(int i=0;i<=1000;i++)
ans=max(ans,f[0][i]);
printf("%lld\n",ans);
}
}
[NOI Online #1 入门组] 魔法
(自己做出)
简单题。
设\(f_{i,j}\)表示走到\(i\)用了\(j\)次魔法的最小代价。
但是如果枚举出边转移有后效性。
考虑把路径划分成没有用魔法...用魔法...没有用魔法的连续段,从第一个没有用魔法的连续段把路径切开。
则后面的连续段都是用魔法....没有用魔法的。
前面没有用魔法到\(x\)的代价是最短路,可以用floyd求。
设\(g_i\)表示没有用过魔法到\(i\)的代价。
\(h_{i,j}\)表示开头用了一次魔法,从\(i\)到\(j\)的代价。
可以枚举\(i\)的出边指向点\(k\)求得。
则\(f_{i,j}=\min(f_{k,j-1}+h_{i,k})\)
\(f_{i,1}=g_i\)
用个\((\min,+)\)矩阵快速幂就完事了。
#include<bits/stdc++.h>
using namespace std;
#define N 110
#define int long long
int n,m,k,d[N][N],e[N][N];
struct no{
int a[N][N];
};
no operator*(no x,no y){
no ans;
memset(ans.a,32,sizeof(ans.a));
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
ans.a[i][j]=min(ans.a[i][j],x.a[i][k]+y.a[k][j]);
return ans;
}
no qp(no x,int y){
no ans;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans.a[i][j]=d[i][j];
for(;y;y>>=1,x=x*x)
if(y&1)
ans=ans*x;
return ans;
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&k);
memset(d,32,sizeof(d));
memset(e,32,sizeof(e));
for(int i=1;i<=n;i++)
d[i][i]=0;
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
d[x][y]=z;
e[x][y]=z;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
no po;
memset(po.a,63,sizeof(po.a));
for(int i=1;i<=n;i++)
po.a[i][i]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++)
if(e[i][k]<=100000000000000ll){
po.a[i][j]=min(po.a[i][j],-e[i][k]+d[k][j]);
}
}
po=qp(po,k);
printf("%lld",po.a[1][n]);
}
学校食堂
(瞄了一眼题解)
考虑dp。
一个位置给前面的限制是:前面所有数\(\leq i+b_i\)
一个数受到后面的限制等于空着的位置的\(\min(i+b_i)\)
随着空位的逐渐填满,\(\min(i+b_i)\)是递增的,且值最多是空着最小数\(+8\)。
这说明能够填数的区间是滑动窗口,且\([\)后面\(+9,\inf]\)全是空的。
所以我们状压最后的数(肯定离空位最小标号很近),最小的空位\(i\),\(i\)后面\(8\)个元素的状态即可。
apio2014 beads
(自己做出)
引理:问题可以被转化为:用若干条直上直下的包含\(2\)条边的链覆盖整棵树,每条边最多只能被覆盖一次,最大被覆盖的边的价值。
证明:由于蓝边不能分裂,这说明了一条边只能被覆盖一次。
一条红边被裂掉后肯定是直上直下的。
如果得知了划分方法,可以这么构造:
把所有划分出来的链(包括红链)按照顶部深度从小到大排序。
遇到一条长度为\(2\)的链,就先插入红边后分裂。否则直接插入红边。
用dp解决这个问题。
设\(f_x\)表示\(x\)节点没有向它的父亲连边的方案,\(g_{x,y}\)表示\(x\)节点向上延伸一个点,向下延伸到\(y\)的最大价值。
显然只有\(O(n)\)类可能的\(g\)。
转移:\(f_x=\sum_{son}\max(f_{son},g_{son,})\)
\(g_{x,y}=\sum_{son\neq y}\max(f_{son},g_{son,})+f_y+w_{x,fa}+w_{x,y}\)
显然求出\(\sum_{son}\max(f_{son},g_{son,})\)后就能快速计算答案。
但是时间复杂度是\(O(n^2)\)的。
考虑换根,用个set维护\(\max(g_{x,})\)即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 500010
int n,h[N],v[N],nxt[N],w[N],f[N],mx[N],fw[N],ans,ec;
multiset<int>s[N];
void add(int x,int y,int z){
v[++ec]=y;
w[ec]=z;
nxt[ec]=h[x];
h[x]=ec;
}
void dfs(int x,int fa){
for(int i=h[x];i;i=nxt[i])
if(v[i]!=fa){
fw[v[i]]=w[i];
dfs(v[i],x);
f[x]+=max(f[v[i]],mx[v[i]]+w[i]);
}
for(int i=h[x];i;i=nxt[i])
if(v[i]!=fa)
s[x].insert(-max(f[v[i]],mx[v[i]]+w[i])+f[v[i]]+w[i]);
if(s[x].size())
mx[x]=f[x]+*s[x].rbegin();
else
mx[x]=-1e12;
}
void rt(int x,int y){
s[y].erase(s[y].find(-max(f[x],mx[x]+fw[x])+f[x]+fw[x]));
f[y]-=max(f[x],mx[x]+fw[x]);
fw[y]=fw[x];
fw[x]=-1e12;
if(s[y].size())
mx[y]=f[y]+*s[y].rbegin();
else
mx[y]=-1e12;
f[x]+=max(f[y],mx[y]+fw[y]);
s[x].insert(-max(f[y],mx[y]+fw[y])+f[y]+fw[y]);
mx[x]=f[x]+*s[x].rbegin();
}
void dd(int x,int fa){
ans=max(ans,f[x]);
for(int i=h[x];i;i=nxt[i])
if(v[i]!=fa){
rt(v[i],x);
dd(v[i],x);
rt(x,v[i]);
}
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<n;i++){
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
fw[1]=1e12;
dfs(1,0);
dd(1,0);
printf("%lld",ans);
}
lg3354
(看了题解)
考虑dp。
设\(f_{x,i}\)表示\(x\)子树的代价被计算好,子树内有\(i\)个伐木场的代价。
然而计算当前点的代价还需要知道它上面第一个点的位置。
设\(f_{x,i,y}\)表示\(x\)子树的代价被计算好,子树内有\(i\)个伐木场,上面第一个伐木场在\(y\)的代价。
转移可以对于\(i\)这一维背包,枚举当前点的每个父亲作为\(y\)。
#include<bits/stdc++.h>
using namespace std;
#define N 210
#define int long long
int n,k,h[N],v[N*2],nxt[N*2],w[N*2],d[N],f[N][N][N],st[N],tp,ec,a[N],tt[N][N];
void add(int x,int y,int z){
v[++ec]=y;
w[ec]=z;
nxt[ec]=h[x];
h[x]=ec;
}
void d1(int x,int fa){
st[++tp]=x;
if(x!=1)
f[x][x][1]=0;
else
f[x][x][0]=0;
for(int i=1;i<tp;i++)
f[x][st[i]][0]=(d[x]-d[st[i]])*a[x];
for(int i=h[x];i;i=nxt[i])
if(v[i]!=fa){
d[v[i]]=d[x]+w[i];
d1(v[i],x);
memset(tt,31,sizeof(tt));
for(int j=1;j<=tp;j++){
for(int a=0;a<=k;a++)
for(int b=0;b<=k-a;b++)
tt[st[j]][a+b]=min(tt[st[j]][a+b],f[x][st[j]][a]+f[v[i]][st[j]][b]);
for(int a=0;a<=k;a++)
for(int b=0;b<=k-a;b++)
tt[st[j]][a+b]=min(tt[st[j]][a+b],f[x][st[j]][a]+f[v[i]][v[i]][b]);
}
for(int j=1;j<=tp;j++)
for(int a=0;a<=k;a++)
f[x][st[j]][a]=tt[st[j]][a];
}
tp--;
}
signed main(){
memset(f,31,sizeof(f));
scanf("%lld%lld",&n,&k);
n++;
for(int i=2;i<=n;i++){
int y,z;
scanf("%lld%lld%lld",&a[i],&y,&z);
y++;
add(y,i,z);
add(i,y,z);
}
d1(1,0);
printf("%lld",f[1][1][k]);
}
lg4766
(看了题解)
降智好题
考虑区间dp,设\(f_{l,r}\)表示消灭出现时间在区间\([l,r]\)的敌人的最小代价。
观察到,一个区间距离最大的敌人肯定是要被消灭的。
消灭完后,肯定当前发射时间之内所有敌人也被消灭。
考虑它的位置\(p\),则枚举发射冲击波的位置\(k\),\(f_{l,r}=\max(f_{l,k-1}+f_{k+1,r}+va_k)\)
#include<bits/stdc++.h>
using namespace std;
#define N 1000
#define int long long
int f[N][N],n,a[N],b[N],d[N],st[N],tp,ia[N],ib[N];
signed main(){
int T;
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);
memset(f,0,sizeof(f));
tp=0;
for(int i=1;i<=n;i++)
scanf("%lld%lld%lld",&a[i],&b[i],&d[i]);
for(int i=1;i<=n;i++){
st[++tp]=a[i];
st[++tp]=b[i];
}
sort(st+1,st+tp+1);
tp=unique(st+1,st+tp+1)-st-1;
for(int i=1;i<=n;i++){
ia[i]=lower_bound(st+1,st+tp+1,a[i])-st;
ib[i]=lower_bound(st+1,st+tp+1,b[i])-st;
}
for(int le=1;le<=tp;le++)
for(int l=1;l<=tp;l++){
int r=l+le-1,po=-1,mx=0;
if(r>tp)
continue;
for(int k=1;k<=n;k++)
if(l<=ia[k]&&ib[k]<=r&&mx<d[k]){
po=k;
mx=d[k];
}
f[l][r]=1e18;
if(po==-1){
f[l][r]=0;
continue;
}
for(int k=l;k<=r;k++)
if(ia[po]<=k&&k<=ib[po]){
f[l][r]=min(f[l][r],f[l][k-1]+f[k+1][r]+mx);
}
}
printf("%lld\n",f[1][tp]);
}
}
购物
(瞄了一眼题解)
观察题目条件,可以发现:
如果两个区间有包含,我们把被包含的区间去掉。
把剩下的区间按照左端点从小到大排序,则第\(i\)个区间不可能和标号\(>i+2\)的区间相交。
且被包含的区间只和一个剩下的区间相交。
考虑每个被包含的区间,它事实上是钦定了一个区间内部的rgb颜色数,且这个区间内部没有其它限制。
用组合数算出方案,然后把这个区间包含的数和这个区间去掉。
考虑把整个数轴从所有被覆盖的线段的左/右端点切开。
如果我们知道了只被覆盖\(1\)次区间的选择状况,则我们可以推知被覆盖\(2\)次区间的选择状况。
考虑dp,设\(f_{i,x,y}\)表示dp到第\(i\)个只被覆盖\(1\)次的区间,现在填了\(x\)个r,\(y\)个b。
转移显然。
在转移完这个区间后,我们显然能够算出下一个覆盖\(2\)次的区间的rgb个数。
用组合数算个数后跳到下一个区间。
假设前面的rg个数分别为\(x,y\),则\(f_{i,x,y}\)赋值为方案数,并且运行上一次的dp即可。
Shopping
(自己做出)
简单题。
2018年集训队论文阐述了这道题的做法:
考虑钦定一个点被选择,把这个点定为根。
那么事实上就是个经典的dfs序树形背包问题了。
可以树分治,钦定重心被选择,做一遍树形背包。
然后把重心删除,递归每个连通块处理。
lg4728
(看了题解)
这种简单题还要看题解。。。。
设\(f_i\)表示末尾为\(i\),另一个序列的最小值。
然而这还不够,我们还要知道当前序列选了多少个。
设\(f_{i,j}\)表示末尾是\(i\),当前末尾是\(i\)的序列有\(j\)个数的另一个序列的最小末尾。
设最小末尾是因为贪心的想,末尾越小越好。
这样子的正确性是我们选了前\(i\)个数时,由于每个数都要被取,所以\(i\)也是必须要被取的。
转移:\(f_{i,j}=\min(f_{i,j},f_{i-1,j-1}),a_{i-1}<a_i\)表示末尾和当前划分到同一个序列内。
\(f_{i,j}=\min(f_{i,j},a_i),a_i>f_{i-1,i-j}\)表示末尾和当前划分到不同序列内。
#include<bits/stdc++.h>
using namespace std;
#define N 2010
int T,f[N][N],n,a[N];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
memset(f,63,sizeof(f));
f[0][0]=a[0]=-1e9;
for(int i=1;i<=n;i++)
for(int j=1;j<=min(n/2,i);j++){
if(a[i]>a[i-1])
f[i][j]=min(f[i][j],f[i-1][j-1]);
if(a[i]>f[i-1][i-j])
f[i][j]=min(f[i][j],a[i-1]);
}
if(f[n][n/2]<1e9)
puts("Yes!");
else
puts("No!");
}
}
lg6573
(自己做出)
简单题,我秒了。
考虑分层图,把图按照除\(k\)下取整分组。
显然每组的点数最多有\(k\)个。
一条边会从\(i\)组到第\(i+1\)组。
设\(f_{i,j}\)表示在第\(i\)组第\(j\)个节点的最短路。
则\(f_{i+1,k}=\min(f_{i,j}+w_{j,k})\)。
这事实上是\(f\)乘以一个矩阵,就是\((\min,+)\)矩阵乘法。
多组询问用线段树维护即可。
#include<bits/stdc++.h>
using namespace std;
#define N 200010
int n,m,id[N],k,q,ok;
struct no{
int a[5][5];
}a[N],bz,b[N],ans;
no operator *(no x,no y){
no ans;
memset(ans.a,63,sizeof(ans.a));
for(int i=0;i<5;i++)
for(int j=0;j<5;j++)
for(int k=0;k<5;k++)
ans.a[i][j]=min(ans.a[i][j],x.a[i][k]+y.a[k][j]);
return ans;
}
void qu(int o,int l,int r,int x,int y){
if(r<x||y<l)
return;
if(x<=l&&r<=y){
if(!ok){
ok=1;
ans=a[o];
}
else
ans=ans*a[o];
return;
}
int md=(l+r)/2;
qu(o*2,l,md,x,y);
qu(o*2+1,md+1,r,x,y);
}
void bd(int o,int l,int r){
if(l==r){
a[o]=b[l];
return;
}
int md=(l+r)/2;
bd(o*2,l,md);
bd(o*2+1,md+1,r);
a[o]=a[o*2]*a[o*2+1];
}
int main(){
scanf("%d%d%d%d",&k,&n,&m,&q);
for(int i=0;i<=n;i++)
memset(b[i].a,63,sizeof(b[i].a));
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
b[x/k].a[x%k][y%k]=z;
}
bd(1,0,n/k);
for(int i=1;i<=q;i++){
int x,y;
scanf("%d %d",&x,&y);
ok=0;
qu(1,0,n/k,x/k,y/k-1);
if(x/k==y/k){
puts("-1");
continue;
}
if(ans.a[x%k][y%k]>1e9)
puts("-1");
else
printf("%d\n",ans.a[x%k][y%k]);
}
}
CF938F
(瞄了一眼题解)
简单题做了这么久。。。
考虑按位贪心,我们要让第一位,第二位,第三位...尽量小。
考虑我们现在枚举第\(i\)位要尽量小,那么我们考虑可能的候选位置\(j\),我们需要快速判定是否符合要求。
考虑我们上一次保留的末尾位置\(x\),我们可以得知\(x\)之前删去的长度集合。
先考虑下一次填的数的最小值,枚举位置\(z\)和\(z-i\)的二进制子集\(p\)。
如果\(p\)在之前合法则可以填入。
我们事实上通过这个过程也可以钦定前缀后哪些长度集合有解。
但是这样子是\(3^{\log_2n}n\)的。
考虑优化,我们事实上是判定\(j-i\)是否有合法的子集,高维前缀和即可。
#include<bits/stdc++.h>
using namespace std;
#define N 5010
char s[N];
int f[N],n,l;
int main(){
scanf("%s",s+1);
n=strlen(s+1);
l=log2(n);
int p=(1<<l);
for(int i=0;i<p;i++)
f[i]=1;
for(int i=1;i<=n-p+1;i++){
int mn=1e9;
for(int j=0;j<p;j++)
for(int k=0;k<l;k++)
if(!(j&(1<<k)))
f[j+(1<<k)]|=f[j];
for(int j=0;j<p;j++)
if(f[j])
mn=min(mn,(int)s[i+j]);
for(int j=0;j<p;j++)
if(s[i+j]!=mn)
f[j]=0;
putchar(mn);
}
}
[JSOI2016]位运算
(瞄了一眼题解)
最后的暴力部分还想用组合数计算。。。。
注意到奇怪的数据范围,所以考虑状态压缩。
把所有数\(p_1,p_2...p_n\)从小到大排序,从高到低填数。
设\(f_{i,t}\)表示填了\(i\)位,如果\(t_j=0\)则\(p_j=p_{j+1}\)
如果\(t_n=0\)则\(p_n=s_i\),否则\(p_n\neq s_i\)
转移时,枚举每一位填的数,判定\(1\)个数是否为奇数,按照题意转移。
注意数的上限。
我们发现\(s\)是循环给出的,所以转移\(k\)次后,每次转移是相同的。
所以可以用矩阵乘法优化计算。
从itst学来另一个做法。
忽略排序和不相同的限制,可以用数位dp解决。
所有数都是一样的。
设\(f_{i,j}\)表示填了前\(i\)个数,目前确定有\(j\)个符合要求。
转移比较显然。
像前面一样用矩阵乘法优化。
考虑容斥求出原问题的解,我们容斥后事实上就是枚举\(n\)个数字的集合划分。
由于出现偶数次的数的xor和是\(0\),所有出现奇数次的数的xor和也是\(0\)。
假设出现奇数次的数的个数是\(s\)。
事实上我们就是要求出\(s\)个数,它们都要\(<R\)且互不相同的方案。
把\(0\to n\)的答案求出即可。
小Y和恐怖的奴隶主
(自己做出)
简单题,我秒了。
发现奇怪的数据范围。
设\(f_{i,a,b,c}\)表示现在第\(i\)轮,有\(a\)个\(3\)血随从,\(b\)个\(2\)血随从,\(c\)个\(1\)血随从。
转移枚举我们打的是\(3\)还是\(2\)还是\(1\)血随从,然后显然能够轻松转移。
注意到我们的转移是乘以一个矩阵,所以可以矩阵快速幂优化。
这还是太慢,用矩阵乘向量优化即可。
CF79D
(看了题解)
考虑差分,问题被转化成了:有一个集合\(s\),有一个序列\(a\)。
可以选择\(a_l,a_{l+s_i}\)反转,问把\(a\)变成全\(0\)的最小代价。
不知道如何证明,我们把两个\(0\)变成两个\(1\)是不优的。
而从\(1\)变成\(0\)事实上是移动\(1\)。
我们只会把两个\(1\)配对,配对的代价可以最短路。
问题转化成有\(2k\)个位置,\(i\)位置和\(j\)位置配对有\(b_{i,j}\)代价,求最小配对代价。
可以一般图最大权匹配,但是数据范围较小,显然可以状态压缩dp。
自己还是不擅长图论...
#include<bits/stdc++.h>
using namespace std;
#define N 100010
int n,k,l,vi[N],di[N],s[100][100],a[N],p[N],id[N],ct,st[N],f[N*20];
queue<int>q;
void dij(int o){
memset(vi,0,sizeof(vi));
memset(di,32,sizeof(di));
di[o]=0;
q.push(o);
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=1;i<=l;i++){
if(t>=a[i]&&!vi[t-a[i]]){
vi[t-a[i]]=1;
di[t-a[i]]=di[t]+1;
q.push(t-a[i]);
}
if(t+a[i]<=n&&!vi[t+a[i]]){
vi[t+a[i]]=1;
di[t+a[i]]=di[t]+1;
q.push(t+a[i]);
}
}
}
for(int i=1;i<=ct;i++)
s[id[o]][i]=di[st[i]];
}
int main(){
scanf("%d%d%d",&n,&k,&l);
for(int i=1;i<=k;i++){
int x;
scanf("%d",&x);
p[x]^=1;
}
for(int i=0;i<=n;i++){
p[i]^=p[i+1];
if(p[i]){
id[i]=++ct;
st[ct]=i;
}
}
for(int i=1;i<=l;i++)
scanf("%d",&a[i]);
for(int i=0;i<=n;i++)
if(id[i])
dij(i);
memset(f,63,sizeof(f));
f[0]=0;
for(int i=0;i<(1<<ct);i++){
int po=-1;
for(int j=0;j<ct;j++)
if(i&(1<<j)){
po=j;
break;
}
if(po==-1)
continue;
for(int j=0;j<ct;j++)
if((i&(1<<j))&&j!=po)
f[i]=min(f[i],f[i^(1<<j)^(1<<po)]+s[j+1][po+1]);
}
if(f[(1<<ct)-1]<1e8)
printf("%d\n",f[(1<<ct)-1]);
else
puts("-1");
}
lg4757
(瞄了一眼题解)
考虑dp,观察到奇怪的数据范围,设\(f_{i,j}\)表示\(i\)的子树,\(i\)连出的儿子边占用情况为\(j\)
把路径挂在lca上考虑。
我们假设选了一条路径,他会占用\(i\)的两个/一个连出边\(x,y\)
它的代价可以用前缀和快速计算。
现在,我们枚举当前节点的两个被占用的点\(x,y\),则\(f_{i,j}=\max(f_{i-{x}-{y}}+val)\)
发现我们对于每一对\(x,y\),只需要存储代价最大的就行了。
abc176F
(自己做出)
考虑dp。
观察题目性质,可以发现:假设我们现在删除到\(i\)位,则\(3i,3i+1,3i+2\)位是新插入的,前面有两位是原来的。
假设原来的位是\(x,y\),现在的是\(3i,3i+1,3i+2\),则可以发现\(3i+3,3i+4\)在这个集合内产生。
设\(f_{i,x,y}\)表示\(i\)位置,原来两位是\(x,y\)的最大值,转移显然。
然而这样子太慢了。
考虑枚举\(i\)后用某种数据结构维护\(f_{i}\)和\(f_{i-1}\)之间的转移。
分类讨论:
新的\(x,y\)都是原来的:不用处理,按照\(a_{3i},a_{3i+1},a_{3i+2}\)是否相同判定\(f_{i}\)是否要\(+1\)。
新的\(x,y\)有一个是原来的:我们事实上要求数组的一行/一列的最大值。
新的\(x,y\)没有一个是原来的:要求整个数组的最大值,可以被拆成\(n\)次对一行/一列最大值的查询。
需要支持单点查询,单点修改。
显然可以维护一行/一列的最大值和加法标记。
由于每次每个数组只会加,不会减,所以在修改时只需要把对应行列与当前值取\(\max\)即可。