2013 Multi-University Training Contest 4
HDU-4632 Palindrome subsequence(区间dp)
题意:求一个字符串有多少个子序列是回文串。
题解:dp[i][j]表示i到j有多少的自序列是回文串,具体转移看代码
代码:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int mod = 10007; char s[1010]; int dp[1010][1010]; int n; int main(){ int T; scanf("%d", &T); for(int t = 1; t<=T; t++){ memset(dp, 0, sizeof(dp)); scanf("%s", s+1); n = strlen(s+1); for(int j = 1; j<=n; j++){ dp[j][j] = 1; for(int i = j-1; i>=1; i--){ if(j-1>=i+1){ dp[i][j] = (dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+mod)%mod; if(s[i] == s[j]){ dp[i][j] = (dp[i][j]+dp[i+1][j-1]+1)%mod; } } else{ if(s[i] == s[j]){ dp[i][j] = 3; } else{ dp[i][j] = 2; } } } } printf("Case %d: %d\n", t, dp[1][n]%mod); } return 0; }
HDU-4635 Strongly connected(强连通缩点)
题意:给一个有向图,问能加的最大边数,使得加完边之后的图任然不是强连通。
题解:显然要使加的边数最大,那么加完后的图应该只剩两个强连通分量,设其中一个强连通分量点的个数为x,另一个为y,则x+y=n。然后考虑整个图最多的边数是多少。在一个强连通分量中,边数最多为x*(x-1),另一个为y*(y-1),然后是所有的x点向所有的y点连边(或者反过来,因此一个强连通分量必然是出度为0或者入度为0)有x*y条边,所以总边数便是x*(x-1)+y*(y-1)+x*y=n*n-n-x*y。我们找一个最小的x*y,然后总边数-m就是答案了。
代码:
#include<iostream> #include<cstdio> #include<cstring> #include<queue> #define ll long long using namespace std; const int maxn = 100010; int n, m; vector<int> G[maxn]; vector<int> scc[maxn]; int dfn[maxn], low[maxn], sta[maxn], ins[maxn], c[maxn], num, top, cnt; int in[maxn], out[maxn]; int Max(ll a, ll b){ return a>b?a:b; } void init(){ for(int i = 1; i<=maxn; i++){ G[i].clear(); scc[i].clear(); } num = 0; top = 0; cnt = 0; memset(dfn, 0, sizeof(dfn)); memset(low, 0, sizeof(low)); memset(sta, 0, sizeof(sta)); memset(ins, 0, sizeof(ins)); memset(c, 0, sizeof(c)); memset(in, 0, sizeof(in)); memset(out, 0, sizeof(out)); } void tarjan(int x){ dfn[x] = low[x] = ++num; sta[++top] = x; ins[x] = 1; for(int i = 0; i<G[x].size(); i++){ int v = G[x][i]; if(!dfn[v]){ tarjan(v); low[x] = min(low[x], low[v]); } else if(ins[v]){ low[x] = min(low[x], low[v]); } } if(dfn[x] == low[x]){ cnt++; int y; do{ y = sta[top--], ins[y] = 0; c[y] = cnt, scc[cnt].push_back(y); }while(x!=y); } } int main(){ int T; scanf("%d", &T); for(int t = 1; t<=T; t++){ scanf("%d %d", &n, &m); init(); for(int i = 1; i<=m; i++){ int u, v; scanf("%d %d", &u, &v); G[u].push_back(v); } for(int i = 1; i<=n; i++){ if(!dfn[i]) tarjan(i); } printf("Case %d: ", t); if(cnt == 1){ printf("-1\n"); continue; } for(int i = 1; i<=n; i++){ for(int j = 0; j<G[i].size(); j++){ int v = G[i][j]; if(c[i]!=c[v]){ out[c[i]]++; in[c[v]]++; } } } ll ans = 0; for(int i = 1; i<=cnt; i++){ if(in[i]&&out[i]) continue; int tmp = scc[i].size(); ans = Max(ans, (ll)n*n-(ll)n-(ll)(n-tmp)*tmp-(ll)m); } printf("%lld\n", ans); } return 0; }
HDU-4638 Group(莫队)
题意:有一个1到n的序列,问在某一个区间内,能将其中的数字最少分为几组,要求每一组的数字都是连续的。
题解:裸的莫队。
代码:
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> #include<algorithm> using namespace std; #define MAXN 100005 #define ll long long struct Query{ int L,R,id; }q[MAXN]; int s,vis[MAXN],id[MAXN]; ll ans[MAXN]; ll res; bool cmp(Query a,Query b){ if(a.L/s == b.L/s) return a.R<b.R; return a.L/s < b.L/s; } void add(int idx){//加入编号为idx的点 if(vis[idx-1] && vis[idx+1]) res--;//如果两边都是连续的,那么组可以减一 else if(!vis[idx-1] && !vis[idx+1]) res++;//如果两边都是断开的,那么加入该点可以使组加一 vis[idx]=1; } void dec(int idx){ if(vis[idx-1] && vis[idx+1]) res++; else if(!vis[idx-1] && !vis[idx+1]) res--; vis[idx]=0; } int main(){ int n,m,t; scanf("%d",&t); while(t--){ memset(vis,0,sizeof vis); scanf("%d%d",&n,&m); s=(int)sqrt(n); for(int i=1;i<=n;i++) scanf("%d",&id[i]); for(int i=0;i<m;i++){ scanf("%d%d",&q[i].L,&q[i].R); q[i].id=i; } sort(q,q+m,cmp); int L=1,R=0; res=0; for(int i=0;i<m;i++){ while(R<q[i].R){ R++; add(id[R]); } while(R>q[i].R){ dec(id[R]); R--; } while(L<q[i].L){ dec(id[L]); L++; } while(L>q[i].L){ L--; add(id[L]); } ans[q[i].id]=res; } for(int i=0;i<m;i++) printf("%d\n",ans[i]); } return 0; }
HDU-4639 Hehe(找规律+斐波那契)
题意:给一个字符串,hehe能有wqnmlgb替换,问该字符串最终能有几种表达方式。
题解:发现几个hehe连在一起的表达方式种数符合斐波那契数列,然后最后乘法原理就行。
代码:
#include<iostream> #include<cstdio> #include<cstring> #include<queue> #include<vector> #define ll long long using namespace std; const int mod = 10007; char s[20010]; int f[20010]; int len; vector<int> G; void init(){ f[0] = 1; f[1] = 1; f[2] = 2; f[3] = 3; for(int i = 4; i<=20005; i++){ f[i] = (f[i-1]%mod+f[i-2]%mod)%mod; } } int main(){ init(); int T; scanf("%d", &T); for(int t = 1; t<=T; t++){ G.clear(); scanf("%s", s+1); len = strlen(s+1); int i = 1; int tmp = 0; while(i<=len+1){ if(s[i] == 'h' && s[i+1] == 'e'){ tmp++; i+=2; } else{ if(tmp) G.push_back(tmp); i++; tmp = 0; } } ll ans = 1; for(i = 0; i<G.size(); i++){ ans = ((ans%mod)*(f[G[i]]%mod))%mod; } printf("Case %d: %lld\n", t, ans%mod); } return 0; }
HDU-4642 Fliping game(矩阵博弈)
题意:给一个01矩阵,每次可以选1的块作为左上角,然后将其右下部分的矩阵全部翻转,最后不能翻转的人输。
题解:对于最右下角的那个矩阵,每次都会将其翻转,先手翻转为1的话永远赢不了,翻转为0的话,后手如果已经不能翻转为1,先手就赢了;如果后手能翻转为1,那么游戏继续。
代码:
#include<iostream> #include<cstdio> #include<cstring> #include<queue> #include<vector> #define ll long long using namespace std; int ma[110][110]; int n, m; int main(){ int T; cin>>T; while(T--){ cin>>n>>m; for(int i = 1; i<=n; i++){ for(int j = 1; j<=m; j++){ cin>>ma[i][j]; } } if(ma[n][m] == 0) cout<<"Bob"<<endl; else cout<<"Alice"<<endl; } return 0; }
HDU-4641 K-string(后缀自动机)
题意:给一个字符串和k,有两种操作,每次往字符串末尾加一个字符,每次询问字符串中出现至少k次的子串个数是多少?
题解:做的第一道后缀自动机,看了两天算是模模糊糊看懂了。
学习的博客:
https://blog.csdn.net/qq_35649707/article/details/66473069 (有完整的证明很详细,但是我看不懂)
https://blog.csdn.net/cdy1206473601/article/details/80206582 (比较简单的构造讲解)
https://blog.csdn.net/liyuanshuo_nuc/article/details/53561527 (hihocoder讲的基本概念,看了这个才懂)
https://blog.csdn.net/vocaloid01/article/details/82902180 (hihocoder讲的构造方法)
首先记录一下一些性质:
后缀自动机的每个状态定义的是在 一些位置的集合 结束的 子串的集合
每个状态的子串个数=从起始点到该点的路径数=step[p]-step[pre(p)] p表示当前状态
每个状态的子串都是该状态中最长的那个子串的后缀,并且是连续的,直到断开为止。断开的含义是某一个较小的后缀在其他位置结束了,这时候新开一个状态来存他。所以沿着pre指针走,就是走到最大的那个断开的后缀所在的状态。
关于这道题,我们用一个num数组存当前状态中所有子串出现的次数。在构建自动机进行状态复制的时候,num记得也要复制。复制的含义相当于,将某一个状态的子串在某一处断开,较小的后缀送给新的复制的状态。
新加一个字母后,得到的最新状态就是字符串本身以及一些未断开的后缀,沿着pre走,每次走到某个状态,这个状态的num加一。这里可以理解为,这些后缀既在字符串的末尾出现(所以加1),又在别的地方出现。
当到达某个状态是通过加1得到大于等于k后,答案加上该状态的所有子串的个数,也就是step[p]-step[pre(p)] ,p表示当前状态
可以发现,沿着pre走,num值是递增的,因为,较短的后缀一定会在更多的地方出现,大于等于k后,他们本身就已经为答案做出贡献了,就不能再继续走下去了。
但感觉还是很抽象.....
代码:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int maxn = 500010; int n, m, k; int ch[maxn][26]; int pre[maxn], step[maxn]; int last, id, ans; int num[maxn]; void init(){ ans = last = id = 0; memset(pre, 0, sizeof(pre)); memset(num, 0, sizeof(num)); memset(step, 0, sizeof(step)); for(int i = 0; i<=n+m; i++){ memset(ch[i], 0, sizeof(ch[i])); } memset(ch[0], -1, sizeof(ch[0])); pre[0] = -1; step[0] = 0; } void Insert(int c){ int p = last, np = ++id; step[np] = step[p]+1; memset(ch[np], -1, sizeof(ch[np])); num[np] = 0; while(p!=-1 && ch[p][c] == -1){ ch[p][c] = np, p = pre[p]; } if(p == -1) pre[np] = 0; else{ int q = ch[p][c]; if(step[q]!=step[p]+1){ int nq = ++id; memcpy(ch[nq], ch[q], sizeof(ch[q])); num[nq] = num[q]; step[nq] = step[p]+1; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while(p!=-1 && ch[p][c] == q){ ch[p][c] = nq, p = pre[p]; } } else pre[np] = q; } last = np; while(np!=-1 && num[np]<k){ num[np]++; //cout<<"! "<<np<<" "<<num[np]<<endl; if(num[np]>=k){ ans+=step[np]-step[pre[np]]; //cout<<"? "<<np<<" "<<pre[np]<<" "<<step[pre[np]]<<" "<<step[np]<<endl; } np = pre[np]; } } char str[250010]; int main(){ while(scanf("%d %d %d", &n, &m, &k)!=EOF){ scanf("%s", str); init(); int len = strlen(str); for(int i = 0; i<len; i++){ Insert(str[i]-'a'); } /*cout<<id<<endl; for(int i = 0; i<=id; i++){ cout<<pre[i]<<" "<<step[i]<<endl; } for(int i = 0; i<=id; i++){ cout<<i<<": "; for(int j = 0; j<5; j++){ cout<<ch[i][j]<<" "; } cout<<endl; } cout<<ans<<endl;*/ while(m--){ int ch; char c; cin>>ch; if(ch == 2){ cout<<ans<<endl; } if(ch == 1){ cin>>c; Insert(c-'a'); } } } return 0; }