口胡(然而有代码)<第四章>
上章回顾:Link
题目计数:\(162\)。
完了,刷不动题了/kk。
\(151.\) P4409 [ZJOI2006]皇帝的烦恼
有关集合关系的 dp 题,感觉好神仙,qwq。
还要二分,时间复杂度是 \(\mathcal O(n\log n)\)。
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define MAXN 20005
#define read(x) scanf("%d",&x)
int n,a[MAXN];
int minx[MAXN],maxn[MAXN];
int l,r;
int check(int x)
{
for(int i=2;i<=n;i++)
{
minx[i]=max(0,a[i]-(x-(a[1]+a[i-1]-maxn[i-1])));
maxn[i]=min(a[i],a[1]-minx[i-1]);
}
return minx[n]?0:1;
}
int main()
{
read(n);
for(int i=1;i<=n;i++) read(a[i]);
a[0]=a[1];
for(int i=0;i<n;i++) l=max(l,a[i]+a[i+1]);
r=2*l,minx[1]=maxn[1]=a[1];
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
printf("%d\n",l);
return 0;
}
\(152.\) AT4636 When I hit my pocket...
考虑贪心。
我们发现把饼干换成钱却不换回来一定是很亏的...
只有把钱换成饼干而且增值的时候有用,那就等价成花两步,增加 \(b-a\) 个饼干,显然当 \(b-a>2\) 时我们就可以贪心的只选这一步了。
但是我们先要花 \(a-1\) 步才能有基准的 \(a\) 个饼干,对于 \(k\leq a\)(多一步没什么卵用)的,要特判。
剩下的奇偶也有判断,这就是一个恶心的分类讨论啊...
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define ll long long
#define read(x) scanf("%lld",&x)
ll k,a,b;
ll x;
int main()
{
read(k),read(a),read(b);
if(k<=a||b-a<=2ll) return printf("%lld\n",k+1ll),0;
ll x=k-a+1;
if(x&1ll) printf("%lld\n",a+((x-1)>>1)*(b-a)+1ll);
else printf("%lld\n",a+(x>>1)*(b-a));
return 0;
}
这个题好巨啊 Orz
我们先来考虑什么时候可以构造出完整的路线。
一个比较显然的结论是中间不能有 \(0\)。
容易猜到和数字奇偶性有关。
考虑一个偶数,他的性质的从一侧进,从同侧出;奇数相反。
于是考虑经过偶数的走法
如果一股劲走完这些,显然不能去另一边了。
如果走不完,那得保证最左边是偶数,然后才能实现转向回来走完。
我们发现如果是多个奇偶块交错,那么显然奇数一侧进另一侧出的性质只能用一次,不能支持回来了,所以合法的序列一定是:
\(0\),奇数,偶数,奇数,\(0\)
或者是他的子序列。
这里的这些数可能是一段,现在如果你在想 \(\mathcal O(n^4)\) dp(谔谔,好像是暴力),那只能像我一样坐以待毙了/kk
我们设 \(dp_{i,j}\) 为到第 \(i\) 个点,已经是第 \(j\) 段的最小步数,转移就比较显然了。
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define read(x) scanf("%lld",&x)
#define int long long
#define MAXN 200005
int n;
int dp[MAXN][6],a[MAXN];
signed main()
{
read(n);
for(int i=1;i<=n;i++) read(a[i]);
for(int i=0;i<=4;i++) dp[0][i]=0;
for(int i=1;i<=n;i++) for(int j=0;j<=4;j++) dp[i][j]=1ll<<62;
for(int i=1;i<=n;i++)
{
if(!a[i])
{
dp[i][0]=dp[i-1][0];
for(int k=0;k<=4;k++) dp[i][4]=min(dp[i][4],dp[i-1][k]);
for(int k=0;k<=1;k++) dp[i][1]=min(dp[i][1],dp[i-1][k]+2);
for(int k=0;k<=3;k++) dp[i][3]=min(dp[i][3],dp[i-1][k]+2);
for(int k=0;k<=2;k++) dp[i][2]=min(dp[i][2],dp[i-1][k]+1);
}
else if(a[i]&1)
{
dp[i][0]=dp[i-1][0]+a[i];
for(int k=0;k<=4;k++) dp[i][4]=min(dp[i][4],dp[i-1][k]+a[i]);
for(int k=0;k<=1;k++) dp[i][1]=min(dp[i][1],dp[i-1][k]+1);
for(int k=0;k<=3;k++) dp[i][3]=min(dp[i][3],dp[i-1][k]+1);
for(int k=0;k<=2;k++) dp[i][2]=min(dp[i][2],dp[i-1][k]);
}
else
{
dp[i][0]=dp[i-1][0]+a[i];
for(int k=0;k<=4;k++) dp[i][4]=min(dp[i][4],dp[i-1][k]+a[i]);
for(int k=0;k<=1;k++) dp[i][1]=min(dp[i][1],dp[i-1][k]);
for(int k=0;k<=3;k++) dp[i][3]=min(dp[i][3],dp[i-1][k]);
for(int k=0;k<=2;k++) dp[i][2]=min(dp[i][2],dp[i-1][k]+1);
}
}
int ans=1ll<<62;
for(int i=0;i<=4;i++) ans=min(ans,dp[n][i]);
printf("%lld\n",ans);
return 0;
}
一道比较简单的构造题。
考虑把前 \(m\) 个数都设成 \(s\),在都是正数的前提下,不会出现包含他们的合法子序列,然后剩下的越离谱越好。
一种比较自然地想法是 \(s\) 比较小时,剩下的全是 \(10^9\),反之,全是 \(1\)。
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define read(x) scanf("%d",&x)
#define MAXN 100005
int n,m,s;
int main()
{
read(n),read(m),read(s);
for(int i=1;i<=m;i++) printf("%d ",s);
if(s>800000000)
for(int i=m+1;i<=n;i++) printf("%d ",1);
else for(int i=m+1;i<=n;i++) printf("%d ",1000000000);
return puts(""),0;
}
这题怎么一个比一个神仙啊,哦好不是,是我太菜了/ll/ll
开始的想法是用二进制串来代表每个卡片是红色还是蓝色,然后却发现如果数字是重的,就会很麻烦以至于不会做就弃了。
不过一个简单的性质还是容易发现的,就是移动了偶数次就是红面,反之是蓝面(哎,这个 AtCoder 怎么这么爱奇偶性啊......)
新加入一张卡要移动多少次?
我们如果每次从小到大加入,最后的这一张后面早被选过的卡即会产生逆序对,这就是要 swap 的次数,然后这题就做完了。
容易状压,时间复杂度是 \(\mathcal O(2^n n^2)\)
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define read(x) scanf("%d",&x)
#define inf 1000000000
int n,x[20][2];
int dp[1000005][20];
int main()
{
read(n);
for(int i=1;i<=n;i++) read(x[i][0]);
for(int i=1;i<=n;i++) read(x[i][1]);
for(int i=0;i<(1<<n);i++) for(int j=0;j<n;j++) dp[i][j]=inf;
for(int i=0;i<n;i++) dp[1<<i][i]=0;
dp[0][0]=0;
for(int s=1;s<(1<<n);s++)
{
int op=0;
for(int j=0;j<n;j++)
{
if((1<<j)&s) op++;
}
for(int i=0;i<n;i++)
{
if(((1<<i)&s)==0) continue;
int num=0;
for(int j=i+1;j<n;j++) if((1<<j)&s) num++;
//要移动的次数及时逆序对数,上面求的是新增的逆序对数
for(int j=0;j<n;j++)
{
if(j==i) continue;
if(((1<<j)&s)==0) continue;
int t=abs(j-op+2)&1;//蓝面返回1,下同
int r=abs(i-op+1)&1;
if(x[j+1][t]>x[i+1][r]) continue;
dp[s][i]=min(dp[s][i],dp[s^(1<<i)][j]+num);
}
}
}
int ans=inf;
for(int i=0;i<n;i++) ans=min(ans,dp[(1<<n)-1][i]);
if(ans>=inf) puts("-1");
else printf("%d\n",ans);
return 0;
}
\(156.\) P4878 [USACO05DEC]Layout G
容易看出是一道差分约束的题目,但是为了判断是否有可行排列方案,要先建立超级源汇点,判断负环,然后再从一跑最短路。
注意两次 SPFA 之间的清空,时间复杂度为 \(\mathcal O(kn)\)。
#include"iostream"
#include"cstdio"
#include"cmath"
#include"queue"
using namespace std;
#define read(x) scanf("%lld",&x)
#define int long long
#define MAXN 100005
int n,a,b;
int u,v,w;
struct node
{
int to,nxt,w;
}e[MAXN*4];
int head[1005],cnt=0;
queue<int>q;
int vis[MAXN],ma[MAXN];
int dis[MAXN],len[MAXN];
int f=0;
void add(int u,int v,int w)
{
e[++cnt].to=v;
e[cnt].nxt=head[u];
e[cnt].w=w;
head[u]=cnt;
}
void SPFA(int s)
{
q.push(s),vis[s]=1,dis[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
ma[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int j=e[i].to;
if(dis[j]>dis[u]+e[i].w)
{
dis[j]=dis[u]+e[i].w;
len[j]=len[u]+1;
if(!vis[j]) vis[j]=1,q.push(j);
if(len[j]>=n+4){f=-1;return;}
}
}
}
}
signed main()
{
read(n),read(a),read(b);
for(int i=1;i<=a;i++)
{
read(u),read(v),read(w);
if(u>v) swap(u,v);
add(u,v,w),add(v,u,0);
}
for(int i=1;i<=b;i++)
{
read(u),read(v),read(w);
if(u>v) swap(u,v);
add(v,u,-w);
}
for(int i=1;i<=n;i++) add(0,i,0);
for(int i=1;i<n;i++) add(i+1,i,0);
for(int i=0;i<=n;i++) dis[i]=1ll<<60,vis[i]=0,len[i]=0;
SPFA(0);
if(f<0) return puts("-1"),0;
else if(!ma[n]) return puts("-2"),0;
for(int i=1;i<=n;i++) dis[i]=1ll<<60,vis[i]=0,len[i]=0,ma[i]=0;
f=0;
SPFA(1);
if(f<0) puts("-1");
else if(!ma[n]) puts("-2");
else printf("%lld\n",dis[n]);
return 0;
}
\(157.\) AT4704 Snuke the Wizard
发现无论如何移动相对顺序不变,二分最里面的被移出去的位置即可。
#include"iostream"
#include"cstring"
#include"cstdio"
#include"cmath"
using namespace std;
#define MAXN 200005
int n,m,sum;
char s[MAXN];
char op[MAXN],rt[MAXN];
int check1(int x)
{
int opt=x;
char now=s[x];
for(int i=1;i<=m;i++)
{
if(now==op[i])
{
if(rt[i]=='L')
{
opt--;
now=s[opt];
if(!opt) return 1;
}
else
{
opt++;
now=s[opt];
if(opt>n) return 0;
}
}
}
return 0;
}
int check2(int x)
{
int opt=x;
char now=s[x];
for(int i=1;i<=m;i++)
{
if(now==op[i])
{
if(rt[i]=='L')
{
opt--;
now=s[opt];
if(!opt) return 0;
}
else
{
opt++;
now=s[opt];
if(opt>n) return 1;
}
}
}
return 0;
}
int main()
{
scanf("%d%d",&n,&m),sum=n;
scanf("%s",s+1);
for(int i=1;i<=m;i++)
{
cin>>op[i]>>rt[i];
}
int l=1,r=n;
if(!check1(1)) l=0;
else
{
while(l<r)
{
int mid=(l+r)>>1;
if(check1(mid)) l=mid+1;
else r=mid;
}
if(!check1(l)) l--;
}
sum-=l;
l++,r=n;
if(check2(n))
{
while(l<r)
{
int mid=(l+r)>>1;
if(check2(mid)) r=mid;
else l=mid+1;
}
sum=sum-(n-r+1);
}
printf("%d\n",sum);
return 0;
}
半退役蒟蒻水一个不正常写法。
我们发现我们不关心一个位置具体翻的次数,只关心他翻了奇数次还是偶数次。
于是再看这个数据范围,思路就比较明了了。
当然要先求出偶数次翻转的位置有 \(s\) 个。
设 \(dp_{i,j}\) 为 翻了 \(j\) 次,有 \(i\) 个硬币被翻了偶数次的方案数。
显然有 \(dp_{n,0}=1\)。
我们根据小学组合知识可以得到转移方程。
当翻了 \(j\) 个,其中有 \(l\) 个从前是翻了偶数次的,可以得到这一操作的贡献为:
然后你问:这样做之后 \(dp_{s,k}\) 就是答案吗?
当然不是,我们 dp 是是乱选的,而最后是有序的。
我们需要除以所有排法,就是把 \(s\) 个偶数和 \(n-s\) 个奇数排列,奇数和奇数,偶数和偶数之间没有差别。
根据简单的组合知识能够知道,答案是 \(\dfrac{dp_{s,k}}{\dbinom{n}{s}}\),模意义下除法要写逆元哦!
时间复杂度是 \(\mathcal O(n^2k)\)。
#include"iostream"
#include"cmath"
#include"cstdio"
using namespace std;
#define read(x) scanf("%d",&x)
#define MAXN 105
#define ll long long
#define MOD 1000000007
int n,m,k,s;
ll C[MAXN][MAXN],dp[MAXN][MAXN];
char c[MAXN],cc[MAXN];
ll quickpow(ll a,ll b)
{
ll ans=1,base=a;
while(b)
{
if(b&1) ans=ans*base%MOD;
base=base*base%MOD;
b>>=1;
}
return ans%MOD;
}
int main()
{
read(n),read(k),read(m);
scanf("%s",c),scanf("%s",cc);
for(int i=0;i<n;i++) if(c[i]==cc[i]) s++;
for(int i=0;i<=100;i++) C[i][0]=1ll;
for(int i=1;i<=100;i++)
{
for(int j=1;j<=i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}
dp[n][0]=1ll;
for(int i=0;i<k;i++)
{
for(int j=0;j<=n;j++)
{
for(int l=0;l<=min(j,m);l++)
{
if(n-j<m-l) continue;
dp[j+m-2*l][i+1]=(dp[j+m-2*l][i+1]+dp[j][i]*C[j][l]%MOD*C[n-j][m-l]%MOD)%MOD;
}
}
}
printf("%lld\n",dp[s][k]*quickpow(C[n][s],MOD-2)%MOD);
return 0;
}
典型的环形均分纸牌问题,具体可见 Link of P2125。
是中位数模型的经典应用,可以在 \(\mathcal O(n)\sim O(n\log n)\) 内解决。
排个序多简单我才不写线性呢/fad。
#include"algorithm"
#include"iostream"
#include"cstdio"
using namespace std;
#define ll long long
#define MAXN 5000005
int n;
ll a[MAXN],s[MAXN],c[MAXN],p[MAXN];
ll x[MAXN];
ll sum=0,now;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]),sum+=a[i];
sum=sum/(ll)n;
for(int i=1;i<=n;i++) s[i]=sum-a[i];
for(int i=2;i<=n;i++) c[i]=c[i-1]+s[i],p[i]=c[i];
sort(c+1,c+n+1),sum=0;
now=c[(n+1)/2];
for(int i=1;i<=n;i++) sum=sum+abs(now-p[i]);
printf("%lld\n",sum);
return 0;
}
\(159.\) P2115 [USACO14MAR]Sabotage G
显然想到的是一段子区间,预处理前缀和可以 \(\mathcal O(1)\) 查询。
这个复杂度确实不错,给我们乱搞留下了充足的时间。
容易想到相关参量只有 \(l,r\) ,所以考虑模拟退火。
注意不要把初温设的太大,更不要把末温设的太小,这样都是白费时间。
然后你发现还是过不了(,把接受概率调小一些就可以了。
好像我 rp 不太行,跑 \(0.9 \;s\) 都会错,所以改成 \(0.95\) 秒就可以过了!
#include"algorithm"
#include"iostream"
#include"cstdio"
#include"ctime"
#include"cmath"
using namespace std;
#define MAXN 100005
#define read(x) scanf("%d",&x)
int n,a[MAXN];
int x,y,L,R;
int l=2,r;
int s[MAXN];
double ans=1000000000000.00;
double t;
inline double check(int l,int r)
{
return (double)(s[n]-s[r]+s[l-1])/(n-r+l-1);
}
inline void SA()
{
double T=2*n,delta=0.994,T0=0.05;
while(T>=T0)
{
L=x+((1.0*rand()/RAND_MAX*2)-1)*T;
R=y+((1.0*rand()/RAND_MAX*2)-1)*T;
L=min(L,r),L=max(l,L),R=max(L,R),R=min(R,r);
double op=check(L,R);
if(op<ans) x=L,y=R,ans=op;
else if(rand()<=exp((ans-op)/T*100000000)*RAND_MAX) x=L,y=R;
T*=delta;
}
}
inline void work(){while((clock()-t)/CLOCKS_PER_SEC<0.95) SA();}
int main()
{
t=clock();
srand((int)time(0)),srand(19260817),srand((int)time(0));
read(n),r=n-1;
for(register int i=1;i<=n;i++) read(a[i]),s[i]=s[i-1]+a[i];
work();
printf("%.3lf\n",ans);
return 0;
}
\(160.\) P3594 [POI2015]WIL-Wilcze doły
考虑二分。
发现删的越多越好,所以删除只有 \(n-d+1\) 种可能。
比较套路的是二分左端点,前缀和处理一下区间和。
现在的问题是如何确定完全包含在这个区间内的最大的删除量。
发现对于每个区间,内含的可删除区间只有 \(l-d+1\) 个,而且是一个定值,所以可以单调队列。
另外这题比较坑,线段树常数大并且带 \(\log\),而 st 表在 \(2e6\) 的 long long
下无法施展,单调队列就成了首选,时间复杂度是 \(\mathcal O(n\log n)\)。
#include"iostream"
#include"cstdio"
#include"cmath"
using namespace std;
#define readl(x) scanf("%lld",&x)
#define read(x) scanf("%d",&x)
#define MAXN 2000005
#define ll long long
int n,d,loc[MAXN],head,tail;
ll p,a[MAXN],q[MAXN],c[MAXN],s[MAXN];
bool check(int l)
{
head=0,tail=1;
int k=l-d+1;
for(int i=1;i<=n-d+1;i++)
{
while(head>=tail&&c[i]>=q[head]) head--;
q[++head]=c[i],loc[head]=i;
if(i-k+1>loc[tail]) tail++;
if(i>=k)
{
ll now=s[i+d-1]-s[i+d-l-1]-q[tail];
if(now<=p) return true;
}
}
return false;
}
int main()
{
read(n),readl(p),read(d);
for(int i=1;i<=n;i++) readl(a[i]);
for(int i=1;i<=d;i++) c[1]+=a[i];
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
for(int i=2;i<=n-d+1;i++) c[i]=c[i-1]-a[i-1]+a[i+d-1];
int l=d,r=n;
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid)) l=mid+1;
else r=mid;
}
if(!check(l)) l--;
printf("%d\n",l);
return 0;
}
\(161.\) CF854B Maxim Buys an Apartment
找规律题。
考虑没别影响到的点可以是放房子也可以有后面的点影响到,然后找一下规律发现 \(k\) 个房子最大影响长度为 \(3k\),能卖的是 \(2k\)。
注意有很多特判qwq。
#include"iostream"
#include"cstdio"
using namespace std;
#define read(x) scanf("%lld",&x)
#define int long long
int n,k;
signed main()
{
read(n),read(k);
if(k==n||(!k)) return printf("0 0\n"),0;
if(3ll*k>=n)
{
printf("%lld %lld\n",1ll,n-k);
return 0;
}
printf("%lld %lld\n",1ll,2*k);
return 0;
}
\(162.\) P1472 [USACO2.3]奶牛家谱 Cow Pedigrees
设 \(dp_{i,j}\) 为选了 \(i\) 个点,最多 \(j\) 层的方案数,于是可以做到比较显然的 \(\mathcal O(n^2k)\) 计算。
就是这个设法十分神仙/kk
最后输出时差分一下即可。
#include"iostream"
#include"cmath"
#include"cstdio"
using namespace std;
#define MAXN 100
#define read(x) scanf("%d",&x)
#define MOD 9901
int n,m;
int dp[MAXN<<1][MAXN];
int main()
{
read(n),read(m);
for(int i=1;i<=m;i++) dp[1][i]=1;
for(int j=1;j<=m;j++)
{
for(int i=3;i<=n;i+=2)
{
for(int k=1;k<i;k+=2)
{
dp[i][j]=(dp[i][j]+dp[k][j-1]*dp[i-1-k][j-1]%MOD)%MOD;
}
}
}
printf("%d\n",(dp[n][m]-dp[n][m-1]+MOD)%MOD);
return 0;
}