lin4xu杂题的四十多个纯口胡
下决心写一道题写一篇题解。
大概最后能凑成lin4xu杂题的全篇题解。计划十天干掉。
快去关注林老师(id:linfourxu)
以下代码部分省略头文件。当然大多数都没有代码。因为懒。
upd:最后十天显然没有干掉。学了一遍博弈论,还在学决策单调性。
P3295 [SCOI2016] 萌萌哒
考虑暴力,直接并查集合并相同的数,和今天T1一样。
考虑优化这个暴力。因为今天T1题解说要倍增,所以考虑一个长的跟ST表一样的倍增。
具体的,我们对每个位置开个长的跟ST表一样的东西。也就是 \(id[i][j]\) 代表 \(i\) 为左端点长度为 \(2^j\) 的情况。然后每次给你一个区间,把区间长度二进制拆分,对应位置合并,这样单次修改就是 \(O(\log n)\) 的。查询的时候直接从大到小扫段长,把长段对半分成两个短段就行了。
最后统计有多少个不一样的记作 \(cnt\) ,答案就是 \(9\times 10^{cnt-1}\) (因为开头不能是 \(0\) )。
看代码吧。
code
const int mod=1000000007;
int n,m,num,cnt;
int fa[100010*20],id[100010][20];
bool v[100010];
int find(int x){
return fa[x]==x?fa[x]:fa[x]=find(fa[x]);
}
void merge(int x,int y){
x=find(x);y=find(y);fa[y]=x;
}
int qpow(int a,int b){
int ans=1;
while(b){
if(b&1)ans=1ll*ans*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=__lg(n);i++){
for(int j=1;j<=n;j++){
id[j][i]=++num;fa[id[j][i]]=num;//初始化并查集
}
}
for(int i=1;i<=m;i++){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
int len=r1-l1+1;
for(int k=__lg(n);k>=0;k--){//从大到小二进制拆分段长
if(l1+(1<<k)-1<=r1){
merge(id[l1][k],id[l2][k]);
l1+=(1<<k);l2+=(1<<k);//合并对应位置
}
}
}
for(int j=__lg(n);j>=1;j--){
for(int i=1;i+(1<<j)-1<=n;i++){
int x=find(id[i][j]);
if(x==id[i][j])continue;
x%=n;if(!x)x=n;//找到对应的左端点x
merge(id[x][j-1],id[i][j-1]);
merge(id[x+(1<<(j-1))][j-1],id[i+(1<<(j-1))][j-1]);//分成两端合并
}
}
for(int i=1;i<=n;i++){
int x=find(id[i][0]);
x%=n;if(!x)x=n;//统计答案
if(!v[x]){
v[x]=true;cnt++;
}
}
printf("%lld\n",9ll*qpow(10,cnt-1)%mod);
return 0;
}
CF359D Pair of Numbers
单调扫一遍每个点左右能扩展到的长度就行。这个不太好说,直接上代码吧。
有一说一理解了马拉车的复杂度分析之后对于很多单调扫的都有一些帮助。
code
int n,a[300010],l[300010],r[300010],ans[300010];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
int x=i;l[i]=i;
while(x>1&&a[x-1]%a[i]==0){//只要左边能扩展就扩展
l[i]=min(l[i],l[x-1]);
x=l[x-1];//如果能整除a[x-1] 那么就能整除整除a[x-1]的所有数 所以可以直接跳指针
}
}
for(int i=n;i>=1;i--){
int x=i;r[i]=i;
while(x<n&&a[x+1]%a[i]==0){
r[i]=max(r[i],r[x+1]);
x=r[x+1];//同理
}
}
int mx=0,cnt=0;
for(int i=1;i<=n;i++)mx=max(mx,r[i]-l[i]);
for(int i=1;i<=n;i++)if(r[i]-l[i]==mx)ans[++cnt]=l[i];
sort(ans+1,ans+cnt+1);
cnt=unique(ans+1,ans+cnt+1)-ans-1;//别忘了unique
printf("%d %d\n",cnt,mx);
for(int i=1;i<=cnt;i++)printf("%d ",ans[i]);
}
P4151 [WC2011]最大XOR和路径
线性基套路题。复习了一下线性基。然后线性基打挂了结果-3。
套路题,结论是直接把所有环跑出来把权值塞进线性基,然后随便找一条从 \(1\) 到 \(n\) 的链求个最大值就行。
code
#define int long long
struct node{
int v,w,next;
}edge[200010];
int n,m,t,head[50010],ans[50010];
bool v[50010];
void add(int u,int v,int w){
edge[++t].v=v;edge[t].w=w;edge[t].next=head[u];head[u]=t;
}
struct base{
int p[64];
void ins(int x){
for(int i=63;i>=0;i--){
if(!((x>>i)&1))continue;
if(!p[i]){
p[i]=x;break;
}
x^=p[i];
}
}
int query(int x){
for(int i=63;i>=0;i--)x=max(x,x^p[i]);
return x;
}
}p;
void dfs(int x,int val){
v[x]=true;ans[x]=val;//更新当前搜索树链的权值
for(int i=head[x];i;i=edge[i].next){
if(v[edge[i].v])p.ins(ans[x]^ans[edge[i].v]^edge[i].w);//插入环权值
else dfs(edge[i].v,val^edge[i].w);//继续找
}
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dfs(1,0);
printf("%lld\n",p.query(ans[n]));
return 0;
}
P2468 [SDOI2010]粟粟的书架
只口胡了一下,没写。没心思调大数据结构题。
观察可以数据点分治,然后就分治。
前一半数据用不着什么别的东西,直接前缀和乱搞一下就行了。记录两个二维前缀和,一个是权值 \(\ge k\) 的权值和,一个是权值 \(\ge k\) 的数的个数,查询的时候二分一下权值乱搞就行。
后一半数据就是放到主席树上二分。这个应该都会。
CF819B Mister B and PR Shifts
joke3579一眼秒了,但是没有完全秒。
这个题还算水(真不觉得有紫),直接统计一波向右移动的时候有多少点贡献减小,多少点贡献增大。贡献减小的点在贡献为 \(0\) 之后会增大,开个桶统计一下每个时刻变成 \(0\) 的有多少点就行了。
记得开long long。
code
#define int long long
int n,ans,cnt,a1,a2,t[1000010],a[1000010];
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
if(a[i]<=i)a2++;
else a1++,t[a[i]-i]++;//右移贡献减小 记录时间
ans+=abs(a[i]-i);//初始贡献
}
int ret=ans,tim=0;
for(int i=n;i>1;i--){
tim++;
ret-=abs(a[i]-n);
ret+=abs(a[i]-1);//特判挪到最左边的贡献
a2--;//去掉
ret-=a1;ret+=a2;
a1-=t[tim];a2+=t[tim];//加上其他点的贡献 更新两类点
if(a[i]==1)a2++;
else{
a1++;//处理最左边的点的类型
if(tim+a[i]-1<=n)t[tim+a[i]-1]++;//注意这里不判的话会数组越界
}
if(ret<ans){
ans=ret;cnt=tim;
}
}
printf("%lld %lld\n",ans,cnt);
return 0;
}
P2470 [SCOI2007]压缩
亚索。
一眼区间dp,然后不会了。
开始的时候想的是暴力 \(dp[i][j]\) 为 \([i,j]\) 间的最小值,然后分讨有没有重复。然后发现样例都过不去。遂∑。
正解考虑 \(dp[i][j][0/1]\) 为 \([i,j]\) 这段区间放/不放 \(M\) 。然后我们默认在 \(i-1\) 位置有个 \(M\) ,分讨一下。
- 不放 \(M\) :如果这段区间是两个相同子串拼起来那么可以直接一个 \(R\) 搞定,即 \(dp[i][j][0]=\min(dp[i][j][0],dp[i][mid][0]+1)\) 。然后枚举中间断点 \(k\) ,有 \(dp[i][j][0]=\min(dp[i][j][0],dp[i][k][0]+j-k)\) 。右边不是 \(dp[k+1][j][0]\) 是因为我们之前默认前面有一个 \(M\) ,如果直接放上会挂。所以只能原封不动放回来。
- 放 \(M\) :枚举放 \(M\) 的位置 \(k\) ,然后有方程 \(dp[i][j][1]=\min(dp[i][j][1],\min(dp[i][k][0],dp[i][k][1])+\min(dp[k+1][j][0],dp[k+1][j][1])+1)\) 。因为有 \(M\) 了所以后半段随便了。
代码随便写写就行了。答案就是 \(\min(dp[1][n][0],dp[1][n][1])\) 。
code
int n,dp[60][60][2];
char s[60];
bool check(int l1,int l2,int len){
for(int i=0;i<len;i++){
if(s[l1+i]!=s[l2+i])return false;
}
return true;
}
int main(){
memset(dp,0x3f,sizeof(dp));
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)dp[i][i][0]=1;//这里大概不用初始化1的情况 因为你一个字母也没必要放M
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(!(len&1)&&check(i,i+(len>>1),len>>1))dp[i][j][0]=min(dp[i][j][0],dp[i][i+(len>>1)-1][0]+1);
for(int k=i;k<j;k++){
dp[i][j][0]=min(dp[i][j][0],dp[i][k][0]+j-k);
dp[i][j][1]=min(dp[i][j][1],min(dp[i][k][0],dp[i][k][1])+min(dp[k+1][j][0],dp[k+1][j][1])+1);
}
}
}
printf("%d\n",min(dp[1][n][0],dp[1][n][1]));
}
P3121 [USACO15FEB]Censoring G
之前有个单个串匹配的,直接上kmp,把所有的操作存栈,然后如果能匹配到就把栈顶弹出,失配指针跳回去。这个直接把kmp换成AC自动机,然后记录每个位置的失配指针跳到哪里就行了。
然后不要写暴力匹配,要把fail的所有状态传下来。
code
int top,n,num,stk[100010],trie[100010][26],fail[100010],cnt[100010];
int pos[100010];
char s[100010],t[100010];
void ins(char s[]){
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
if(!trie[p][s[i]-'a'])trie[p][s[i]-'a']=++num;
p=trie[p][s[i]-'a'];
}
cnt[p]=len;//标记一下长度
}
queue<int>q;
void build(){
for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<26;i++){
if(trie[x][i]){
fail[trie[x][i]]=trie[fail[x]][i];
cnt[trie[x][i]]+=cnt[fail[trie[x][i]]];//记得传
q.push(trie[x][i]);
}
else trie[x][i]=trie[fail[x]][i];
}
}
}
int main(){
scanf("%s%d",s+1,&n);
for(int i=1;i<=n;i++){
scanf("%s",t);ins(t);
}
build();
int len=strlen(s+1);
int p=0;
for(int i=1;i<=len;i++){
p=trie[p][s[i]-'a'];
pos[i]=p;stk[++top]=i;
if(cnt[p]){
top-=cnt[p];p=pos[stk[top]];//不用说了吧
}
}
for(int i=1;i<=top;i++)printf("%c",s[stk[i]]);
return 0;
}
P3041 [USACO12JAN]Video Game G
套路 \(dp[i][j]\) 为 \(i\) 长度 \(j\) 节点处的最大值,初始化 \(dp[0][0]=0\) 其他 \(-\infty\) 。然后有个显然的转移方程,会AC自动机上dp的应该都能一眼秒。
然后数组开小RE了一发。
code
int n,k,num,trie[5010][26],fail[5010],cnt[5010];
int dp[5010][5010];
char s[2000];
void ins(char s[]){
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
if(!trie[p][s[i]-'A'])trie[p][s[i]-'A']=++num;
p=trie[p][s[i]-'A'];
}
cnt[p]=1;
}
queue<int>q;
void build(){
for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<26;i++){
if(trie[x][i]){
fail[trie[x][i]]=trie[fail[x]][i];
cnt[trie[x][i]]+=cnt[fail[trie[x][i]]];
q.push(trie[x][i]);
}
else trie[x][i]=trie[fail[x]][i];
}
}
}
int main(){
memset(dp,-0x3f,sizeof(dp));
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%s",s);ins(s);
}
build();
dp[0][0]=0;
for(int i=1;i<=k;i++){
for(int j=0;j<=num;j++){
for(int k=0;k<26;k++){
dp[i][trie[j][k]]=max(dp[i][trie[j][k]],dp[i-1][j]+cnt[trie[j][k]]);
}
}
}
int ans=0;
for(int i=0;i<=num;i++)ans=max(ans,dp[k][i]);
printf("%d\n",ans);
return 0;
}
P4606 [SDOI2018]战略游戏
裸跑一个圆方树,然后答案就是某个连通块里没有被选中的圆点个数。直接把所有点拎出来建个虚树,把圆点个数放到边上,dfs一遍加个和就行了。
代码没写。感觉会很长。
P2575 高手过招
跳棋跳过一堆子的情况可以看做把这个连通块的若干棋子都向右移动一格。所以可以看做阶梯Nim,空格自己算一个阶梯,所有联通的棋子看做放在一个阶梯上。最后一格为第 \(0\) 级,所有奇数级阶梯异或就行。
code
int n,a[21],s[21];
int main(){
int tim;scanf("%d",&tim);
while(tim--){
scanf("%d",&n);int ans=0;
for(int i=1;i<=n;i++){
for(int i=0;i<=20;i++)a[i]=s[i]=0;
int m,cnt=0;scanf("%d",&m);
for(int i=1;i<=m;i++){
int x;scanf("%d",&x);a[x]++;
}
for(int i=20;i>=1;i--){
if(!a[i])cnt++;
else s[cnt]++;
}
int ret=0;
for(int i=1;i<=20;i+=2)ret^=s[i];
ans^=ret;
}
if(ans)puts("YES");
else puts("NO");
}
return 0;
}
P3730 曼哈顿交易
直接莫队,然后开个值域分块维护所有出现次数就行。
主席树不会。Muel的那个神仙 \(O(1)\) 查询做法看不懂。
不想调莫队。
P3185 [HNOI2007]分裂游戏
首先显然每个瓶子里的豆子数量可以 \(\bmod\ 2\) 。
然后把每个瓶子里的豆子看做状态, \(O(n^3)\) 枚举三个瓶子算 \(\rm SG\) 和统计 \(\rm SG\) 函数就行了。求解个数和字典序最小的解可以爆搜第一步。
code
int n,a[110],sg[110];
bool v[110];
int main(){
int tim;scanf("%d",&tim);
while(tim--){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),sg[i]=0;
for(int i=n-1;i>=1;i--){
memset(v,false,sizeof(v));
for(int j=i+1;j<=n;j++){
for(int k=j;k<=n;k++)v[sg[j]^sg[k]]=true;
}
while(v[sg[i]])sg[i]++;
}
int ans=0;
for(int i=1;i<=n;i++)if(a[i]&1)ans^=sg[i];
if(ans){
int cnt=0;
bool jud=false;
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++){
for(int k=j;k<=n;k++){
if((ans^sg[i]^sg[j]^sg[k])==0){
cnt++;
if(!jud){
printf("%d %d %d\n",i-1,j-1,k-1);
jud=true;
}
}
}
}
}
printf("%d\n",cnt);
}
else printf("-1 -1 -1\n0\n");
}
return 0;
}
P3165 [CQOI2014]排序机械臂
建议参考[模板]文艺平衡树。
给所有的离散化一下钦定一个顺序,然后保存子树最小值大力查找,区间翻转打标记就行了。
顺带一提我写区间喜欢写Splay,因为不带脑子。
不想调平衡树。
P3480 [POI2009]KAM-Pebbles
考虑差分,差分之后取走一堆里的石子就相当于放到下一堆里,变成阶梯Nim。
注意反着扫。注意循环顺序。注意数组大小。
code
int n,a[1010];
int main(){
int tim;scanf("%d",&tim);
while(tim--){
scanf("%d",&n);int ans=0;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=n;i>=1;i--)a[i]=a[i]-a[i-1];
for(int i=n;i>=1;i-=2)ans^=a[i];
if(ans)puts("TAK");
else puts("NIE");
}
return 0;
}
P3107 [USACO14OPEN]Odometer S
好玩的数位dp。但是调不出来,原因未知。所以没有代码。
枚举主元素 \(0-9\) 进行数位dp,然后这样算会把所有有两个出现次数一半一半的数算重,再数位dp一遍减掉就行。
CF895C Square Subsets
看着这个值域70就很奇怪。筛了一下发现70以内素数19个,直接每个数状压。于是就要求选出一组数异或和为 \(0\) 的方案数。
直接把所有元素塞进线性基,设线性基大小为 \(size\) ,答案就是 \(2^{n-size}-1\) ,因为不能不选。
某毛子网站真慢。
code
const int mod=1000000007;
int n,p[100];
bool v[100];
void get(int n){
for(int i=2;i<=n;i++){
if(!v[i])p[++p[0]]=i;
for(int j=1;j<=p[0]&&i*p[j]<=n;j++){
v[i*p[j]]=true;
if(i%p[j]==0)break;
}
}
}
struct base{
int p[20];
void ins(int x){
for(int i=19;i>=0;i--){
if(!((x>>i)&1))continue;
if(!p[i]){
p[i]=x;break;
}
x^=p[i];
}
}
}b;
int qpow(int a,int b){
int ans=1;
while(b){
if(b&1)ans=1ll*a*ans%mod;
a=1ll*a*a%mod;
b>>=1;
}
return ans;
}
int main(){
get(70);
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
int ret=0;
for(int j=1;j<=p[0];j++){
while(x%p[j]==0)x/=p[j],ret^=1<<(j-1);
}
b.ins(ret);
}
int cnt=0;
for(int i=19;i>=0;i--)if(b.p[i])cnt++;
printf("%d\n",(qpow(2,n-cnt)-1+mod)%mod);
return 0;
}
CF613D Kingdom and its Cities
直接上虚树,然后考虑dp,如果当前点 \(x\) 为关键点那么要断掉所有没断掉的儿子,如果不是那分情况,如果不只有一个没断掉直接断掉自己,否则传到父亲上去和别的一起断掉。
码量大的一律不写代码。懒。
P5785 [SDOI2012]任务安排
这玩意怎么紫了。哦我看错了还有个弱化版是蓝的啊。那没事了。
斜率优化板子。方程一眼 \(dp[i]=dp[j]-(s+sumt[i])\times sumc[j]+sumt[i]\times sumc[i]+s\times sumc[n]\) 。然后开个下凸包切就完事了。注意斜率不一定单调所以得二分。
有点卡精度(卡得过不去样例),除换乘就行了。
code
#include <cstdio>
#include <algorithm>
#include <iostream>
#define int long long
using namespace std;
int n,s,t[300010],c[300010],dp[300010];
int l,r,q[300010],sumt[300010],sumc[300010];
int get(int l,int r,int k){
while(l<r){
int mid=(l+r)>>1;
if(dp[q[mid]]-dp[q[mid+1]]>=k*(sumc[q[mid]]-sumc[q[mid+1]]))l=mid+1;
else r=mid;
}
return q[l];
}
signed main(){
scanf("%lld%lld",&n,&s);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&t[i],&c[i]);
sumt[i]=sumt[i-1]+t[i];
sumc[i]=sumc[i-1]+c[i];
}
for(int i=1;i<=n;i++){
int j=get(l,r,sumt[i]+s);
dp[i]=dp[j]+sumt[i]*(sumc[i]-sumc[j])+s*(sumc[n]-sumc[j]);
while(r&&((dp[i]-dp[q[r]])*(sumc[q[r]]-sumc[q[r-1]])<=(dp[q[r]]-dp[q[r-1]])*(sumc[i]-sumc[q[r]])))r--;
q[++r]=i;
}
printf("%lld\n",dp[n]);
return 0;
}
P1477 [NOI2008] 假面舞会
不会,一会再说。
P2633 Count on a tree
这题严格弱于森林那题。
建个树上主席树,具体的,每个点以它的父亲为前驱节点建主席树,查询一条链的时候直接 \(tree[u]+tree[v]-2\times tree[lca(u,v)]\) 就行。
code
#define lson tree[rt].ls
#define rson tree[rt].rs
struct node{
int v,next;
}edge[200010];
int n,m,t,tot,cnt,ans,head[100010],rt[100010],a[100010],lsh[100010];
void add(int u,int v){
edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int fa[100010][21],dep[100010];
struct tr{
int ls,rs,sum;
}tree[100010<<6];
void build(int &rt,int l,int r){
rt=++tot;
if(l==r)return;
int mid=(l+r)>>1;
build(lson,l,mid);build(rson,mid+1,r);
}
void update(int &rt,int pre,int l,int r,int pos){
rt=++tot;
tree[rt]=tree[pre];tree[rt].sum++;
if(l==r)return;
int mid=(l+r)>>1;
if(pos<=mid)update(lson,tree[pre].ls,l,mid,pos);
else update(rson,tree[pre].rs,mid+1,r,pos);
}
int query(int x,int y,int l,int r,int k,int lc,int f){
if(l==r)return l;
int ret=tree[tree[x].ls].sum+tree[tree[y].ls].sum-tree[tree[lc].ls].sum-tree[tree[f].ls].sum;
int ans,mid=(l+r)>>1;
if(ret>=k)ans=query(tree[x].ls,tree[y].ls,l,mid,k,tree[lc].ls,tree[f].ls);
else ans=query(tree[x].rs,tree[y].rs,mid+1,r,k-ret,tree[lc].rs,tree[f].rs);
return ans;
}
void dfs(int x,int f){
dep[x]=dep[f]+1;fa[x][0]=f;
update(rt[x],rt[f],1,cnt,a[x]);
for(int i=1;i<=20;i++)fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].v!=f)dfs(edge[i].v,x);
}
}
int lca(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=20;i>=0;i--)if(dep[fa[y][i]]>=dep[x])y=fa[y][i];
if(x==y)return x;
for(int i=20;i>=0;i--)if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);lsh[i]=a[i];
}
sort(lsh+1,lsh+n+1);
cnt=unique(lsh+1,lsh+n+1)-lsh-1;build(rt[0],1,cnt);
for(int i=1;i<=n;i++)a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
for(int i=1;i<n;i++){
int u,v;scanf("%d%d",&u,&v);add(u,v);add(v,u);
}
dfs(1,0);
while(m--){
int x,y,k;scanf("%d%d%d",&x,&y,&k);
x^=ans;
int lc=lca(x,y);
ans=lsh[query(rt[x],rt[y],1,cnt,k,rt[lc],rt[fa[lc][0]])];
printf("%d\n",ans);
}
return 0;
}
P5680 [GZOI2017]共享单车
题面长篇大论差评。
最短路树dijkstra跑一遍,然后每次暴力改颜色建虚树套路dp,没了。
我虚树都不会写了。
P3265 [JLOI2015]装备购买
实数线性基板子。虽然但是这才是线性空间的基本定义(
考虑插入一个数的时候的操作。实际上只需要把异或改一下。假设这个向量是 \(p\) ,插入 \(x\) ,扫到第 \(i\) 位,那么设 \(tmp=\frac {x[i]}{p[i]}\) ,那么 \(x\) 变成 \(x-tmp\times p\) 就行了。
code
int n,m;
const long double eps=1e-9;
struct vec{
long double a[510];
vec operator+(const vec &s)const{
vec ret={};
for(int i=1;i<=m;i++)ret.a[i]=a[i]+s.a[i];
return ret;
}
vec operator-(const vec &s)const{
vec ret={};
for(int i=1;i<=m;i++)ret.a[i]=a[i]-s.a[i];
return ret;
}
vec operator*(const long double &x)const{
vec ret={};
for(int i=1;i<=m;i++)ret.a[i]=a[i]*x;
return ret;
}
};
struct node{
vec c;
int val;
bool operator<(const node &s)const{
return val<s.val;
}
}a[510];
struct base{
vec p[510];
bool ins(vec x){
for(int i=m;i>=1;i--){
if(fabs(x.a[i])<=eps)continue;
if(fabs(p[i].a[i])<=eps){
for(int j=1;j<=m;j++)p[i].a[j]=x.a[j];
return true;
}
long double ret=x.a[i]/p[i].a[i];
x=x-p[i]*ret;
}
return false;
}
}p;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)scanf("%Lf",&a[i].c.a[j]);
}
for(int i=1;i<=n;i++)scanf("%d",&a[i].val);
sort(a+1,a+n+1);
int cnt=0,val=0;
for(int i=1;i<=n;i++){
if(p.ins(a[i].c)){
cnt++;val+=a[i].val;
}
}
printf("%d %d\n",cnt,val);
}
P3709 大爷的字符串题
严格弱于P3730 曼哈顿交易,除了数据范围两倍和题面不说人话。
实际上就是区间出现次数最多的元素的出现次数。
CF600E Lomsat gelral
dsu on tree板子之一。开个权值线段树然后按如下步骤:遍历轻儿子,统计答案,不记录影响;遍历重儿子,统计答案,记录影响;遍历轻儿子,记录影响。
统计答案什么的随便写个线段树就行了。记录个区间最大值和权值。还有区间覆盖。
P5217 贫穷
一眼平衡树。
插入删除翻转套路操作,找位置由于字母相对位置不会动那么记录一下初始字符串中的所有字符在树中的位置直接查,记得特判删除。输出第x个字母常规。种类数由于只有26个字母直接状压就行了。
P3537 [POI2012]SZA-Cloakroom
常规背包,题单唯一蓝题。(然而比好多紫难点)
离线把询问按照 \(m\) 排序,属性按 \(a\) 排序,整个指针扫物品,背包出使得 \(c\) 之和为某个值的最小的 \(b\) ,比较就行了。
code
int n,m,mx,dp[100010];
bool ans[1000010];
struct node{
int a,b,c;
bool operator<(const node &s)const{
return a<s.a;
}
}a[1010];
struct ques{
int m,k,s,id;
bool operator<(const ques &s)const{
return m<s.m;
}
}q[1000010];
signed main() {
scanf("%d",&n);
memset(dp,-0x3f,sizeof(dp));
dp[0]=2147483647;
for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i].c,&a[i].a,&a[i].b);
scanf("%d",&m);
for(int i=1;i<=m;i++)scanf("%d%d%d",&q[i].m,&q[i].k,&q[i].s),q[i].id=i,mx=max(mx,q[i].k);
sort(a+1,a+n+1);sort(q+1,q+m+1);
int top=1;
for(int i=1;i<=m;i++){
while(top<=n&&a[top].a<=q[i].m){
for(int j=mx;j>=a[top].c;j--){
dp[j]=max(dp[j],min(dp[j-a[top].c],a[top].b));
}
top++;
}
if(dp[q[i].k]>q[i].m+q[i].s)ans[q[i].id]=true;
}
for(int i=1;i<=m;i++)ans[i]?printf("TAK\n"):printf("NIE\n");
}
P4149 [IOI2011]Race
点分治。然而我好像忘了怎么做了。
每次点分的时候开个桶,记录权值和为 \(w\) 的最小边数,点分的时候一棵一棵子树扫,扫子树的时候使用桶里的数更新答案,每扫完一棵子树更新一次桶。
印象中好像有点卡常。
code
struct node{
int v,w,next;
}edge[400010];
int n,t,k,head[200010],ans;
void add(int u,int v,int w){
edge[++t].v=v;edge[t].w=w;edge[t].next=head[u];head[u]=t;
}
bool v[200010];
int rt,sum,cnt,size[200010],maxx[200010],a[200010];
int dis[1000010],road[200010];
void find(int x,int fa){
size[x]=1;maxx[x]=0;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].v!=fa){
find(edge[i].v,x);
size[x]+=size[edge[i].v];
maxx[x]=max(maxx[x],size[edge[i].v]);
}
}
maxx[x]=max(maxx[x],sum-size[x]);
if(maxx[x]<maxx[rt])rt=x;
}
void getdis(int x,int fa,int w,int d){
if(w>k)return;
a[++cnt]=w;road[cnt]=d;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].v!=fa)getdis(edge[i].v,x,w+edge[i].w,d+1);
}
}
void solve(int x){
cnt=0;dis[0]=0;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]){
int ret=cnt;
getdis(edge[i].v,x,edge[i].w,1);
for(int j=ret+1;j<=cnt;j++)ans=min(ans,dis[k-a[j]]+road[j]);
for(int j=ret+1;j<=cnt;j++)dis[a[j]]=min(dis[a[j]],road[j]);
}
}
for(int i=1;i<=cnt;i++)dis[a[i]]=1000010;
}
void div(int x){
solve(x);v[x]=true;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]){
sum=size[edge[i].v];rt=0;
find(edge[i].v,x);div(rt);
}
}
}
int main(){
memset(dis,0x3f,sizeof(dis));
scanf("%d%d",&n,&k);sum=n;ans=maxx[rt]=n+1;
for(int i=1;i<n;i++){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
u++;v++;add(u,v,w);add(v,u,w);
}find(1,0);div(rt);
if(ans>n)ans=-1;
printf("%d",ans);
return 0;
}
P3515 [POI2011]Lightning Conductor
咕。
P5574 [CmdOI2019]任务分配问题
咕。
P3707 [SDOI2017]相关分析
看见这一堆东西,俩字,爆拆。拆了以后发现只需要维护 \(\sum x,\sum y,\sum xy,\sum x^2\) 。然后把pushup和pushdown也爆拆了就行了。
不想写,所以没写。
P4728 [HNOI2009]双递增序列
挺神的题。
状态定义比较神仙:设 \(dp[i][j]\) 为扫到 \(i\) (此时一个序列的最后一个数一定是 \(i\) ),另一个序列长度为 \(j\) ,长 \(j\) 的序列的最后一个元素的最小值。
转移考虑 \(a_i\) 能接在哪个序列上。
- 能接在结尾为 \(i-1\) 的序列上: \(dp[i][j]=\min(dp[i][j],dp[i-1][j-1])\) 。
- 能接在结尾为 \(dp[i-1][j]\) 的序列上: \(dp[i][i-j]=\min(dp[i][i-j],a[i-1])\) 。由于另一个序列的结尾变成了 \(a[i-1]\) 所以是这个式子。
初始化全 \(\infty\) ,然后 \(dp[1][1]=-\infty\) 。
code
int n,a[2010],dp[2010][2010];
int main(){
int tim;scanf("%d",&tim);
while(tim--){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
memset(dp,0x3f,sizeof(dp));
dp[1][1]=-0x3f3f3f3f;
for(int i=2;i<=n;i++){
for(int j=1;j<=(n>>1);j++){
if(a[i]>dp[i-1][j])dp[i][i-j]=min(dp[i][i-j],a[i-1]);
if(a[i]>a[i-1])dp[i][j]=min(dp[i][j],dp[i-1][j-1]);
}
}
if(dp[n][n>>1]!=0x3f3f3f3f)puts("Yes!");
else puts("No!");
}
return 0;
}
CF845G Shortest Path Problem?
重题。
SP10707 COT2 - Count on a tree II
树上莫队裸题。没啥意思。回头补一发树分块。
P4178 Tree
又是点分治。把子树内所有点到根的距离记录一下,然后可以双指针扫一遍。
code
struct node{
int v,w,next;
}edge[80010];
int n,t,k,head[40010],ans;
void add(int u,int v,int w){
edge[++t].v=v;edge[t].w=w;edge[t].next=head[u];head[u]=t;
}
bool v[40010];
int rt,sum,cnt,dis[40010],size[40010],maxx[40010],a[40010];
void find(int x,int fa){
size[x]=1;maxx[x]=0;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].v!=fa){
find(edge[i].v,x);
size[x]+=size[edge[i].v];
maxx[x]=max(maxx[x],size[edge[i].v]);
}
}
maxx[x]=max(maxx[x],sum-size[x]);
if(maxx[x]<maxx[rt])rt=x;
}
void getdis(int x,int fa){
if(a[x]<=k)dis[++dis[0]]=a[x];
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].v!=fa){
a[edge[i].v]=a[x]+edge[i].w;
getdis(edge[i].v,x);
}
}
}
int solve(int x,int len){
dis[0]=0;a[x]=len;cnt=0;
getdis(x,0);
sort(dis+1,dis+dis[0]+1);
for(int l=1,r=dis[0];r>=l;l++){
while(dis[l]+dis[r]>k)r--;
if(r<l)break;
cnt+=r-l;
}
return cnt;
}
void div(int x){
ans+=solve(x,0);v[x]=true;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]){
ans-=solve(edge[i].v,edge[i].w);
sum=size[edge[i].v];rt=0;
find(edge[i].v,x);div(rt);
}
}
}
int main(){
scanf("%d",&n);sum=n;maxx[rt]=n+1;
for(int i=1;i<n;i++){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}scanf("%d",&k);
find(1,0);
div(rt);
printf("%d",ans);
return 0;
}
P3850 [TJOI2007]书架
显然是平衡树,但是你看着这个东西难道不想写一发vector暴力然后凭借洛谷神机暴力碾标算吗(
code
int n,m,q,cnt,pos;
unordered_map<int,string>mp;
vector<int>v;
string s;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
while(n--){
cin>>s;cnt++;
mp[cnt]=s;v.push_back(cnt);
}
cin>>m;
while(m--){
cin>>s>>pos;cnt++;mp[cnt]=s;
v.insert(v.begin()+pos,cnt);
}
cin>>q;
while(q--){
cin>>pos;cout<<mp[v[pos]]<<endl;
}
return 0;
}
P4360 [CEOI2004] 锯木厂选址
枚举第二个厂的位置 \(i\) ,那么有一个显然的式子。设 \(dp_i\) 为到 \(i\) 的答案, \(dis_i\) 为 \(i\) 到山脚的距离, \(sum_i\) 为重量的前缀和, \(tot\) 为总距离,那么
长得很像斜率优化。
P1852 跳跳棋
不会,摆烂。
P3506 [POI2010]MOT-Monotonicity 2
看见这个东西直接考虑 \(dp[i]\) 为 \(i\) 结尾的序列最长长度,那么转移考虑三种情况,把所有的值放到线段树里维护一下就行了。
P3658 [USACO17FEB]Why Did the Cow Cross the Road III P
看见这个东西考虑一手转化。设两个数在两个排列里的位置分别为 \(pos1_i,pos1_j,pos2_i,pos2_j\) ,那么满足条件的所有数显然为:
- \(pos1_i>pos1_j\)
- \(pos2_i<pos2_j\)
- \(|i-j|>k\)
三维偏序即可。
P2403 [SDOI2010]所驼门王的宝藏
三个门。
每行建一个虚点代表该行的横天门,每列建一个虚点代表该列的纵寰门,任意门暴力连边,最后跑一遍tarjan就行了。
P5335 [THUSC2016]补退选
一上来口胡了一个可持久化trie的做法,每个节点记录历史最大值,然后二分时间找答案。
题解的另一个做法是每次答案更改的时候记录一下时间,因为答案是单调的。
P2414 [NOI2011] 阿狸的打字机
首先把所有字符串模拟一遍打出来建AC自动机。然后发现这个问题就是求 \(x\) 结尾的节点在fail树上的子树内有多少 \(y\) 的节点,离线扫一遍即可。
P2824 [HEOI2016/TJOI2016]排序
暴力可过。请使用桶排。
正解考虑所有操作对序列的这个位置上的数字的影响。离线二分一下答案,把序列中所有超过答案的数标 \(1\) ,其余标 \(0\) ,线段树维护区间 \(01\) 个数,区间覆盖,如果最后这个位置是 \(1\) 那就可行。
P4137 Rmq Problem / mex
经典套路。离线所有询问,按照左端点从小到大排序。首先预处理出所有区间 \(1-i\) 的 \(\rm mex\) ,然后每次从小到大扫左端点,每次处理完一个询问就把右边的所有 \(\rm mex\) 和这个位置上的 \(a\) 取个 \(\min\) 。实现可以吉老师线段树,也可以直接线段树上二分,因为 \(\rm mes\) 是单调的。
P3466 [POI2008]KLO-Building blocks
考虑枚举这个区间,那么我们要做的就是查询一个区间的中位数(这个可以单调扫一遍),找区间和,区间比中位数大/小的值,主席树即可。
P5175 数列
转移矩阵:
code
#define int long long
const int mod=1000000007;
int n,a1,a2,x,y;
struct node{
int a[5][5];
node operator*(const node &s)const{
node ret={};
for(int i=1;i<=4;i++){
for(int j=1;j<=4;j++){
for(int k=1;k<=4;k++){
ret.a[i][j]=(ret.a[i][j]+1ll*a[i][k]*s.a[k][j]%mod)%mod;
}
}
}
return ret;
}
};
node qpow(node a,int b){
node ans={};
for(int i=1;i<=4;i++)ans.a[i][i]=1;
while(b){
if(b&1)ans=ans*a;
a=a*a;
b>>=1;
}
return ans;
}
signed main(){
int tim;scanf("%lld",&tim);
while(tim--){
node a={},b={};
scanf("%lld%lld%lld%lld%lld",&n,&a1,&a2,&x,&y);
b.a[1][1]=1ll*a1*a1%mod;
b.a[2][1]=1ll*a2*a2%mod;
b.a[3][1]=1ll*a1*a1%mod;
b.a[4][1]=1ll*a1*a2%mod;
a.a[1][1]=a.a[1][2]=1;
a.a[2][2]=1ll*x*x%mod;
a.a[2][3]=1ll*y*y%mod;
a.a[2][4]=2ll*x*y%mod;
a.a[3][2]=1;
a.a[4][2]=x;a.a[4][4]=y;
a=qpow(a,n-1);
a=a*b;
printf("%lld\n",a.a[1][1]);
}
}
P6406 [COCI2014-2015#2] Norma
套路分治,然后考虑分治到 \(l,r\) 后所有越过中点 \(mid\) 的区间。分类讨论。枚举左端点,计算所有右端点的贡献。
然后根据我们分治的套路,左右预处理出前缀 \(\min,\max\) ,然后设两个能卡住左端点 \(i\) 的指针 \(j,k\) ,就有如下三种情况:(假设 \(j<k\) )
- \([mid+1,j]\) :可以直接从左边的预处理信息得到,做个等差数列求和就行。
- \([j+1,k]\) :考虑预处理 \(\min,\max\) 两边的前缀和,求的时候直接差分一下。
- \([k+1,r]\) :同样是预处理前缀和即可。