HNOI2008题目总结

时间:2023-03-08 22:45:27
HNOI2008题目总结

呜呼。。NOI前一个月正式开始切BZOJ了……以后的题解可能不会像之前的零散风格了,一套题我会集中起来发,遇到一些需要展开总结的东西我会另开文章详细介绍。

用了一天的时间把HNOI2008这套题切了……感觉新知识好多啊……一定是我太弱了,各方面能力还都需要加强,尤其是DP啦推导啦神马的

BZOJ1004 Cards:

题目大意:

桌上有N张牌,将这N张牌染成sr张红色,sg张绿色和sb张蓝色,然后给出M种洗牌方式,可以通过洗牌得到的方案视作相同方案,问不同的染色方案数。输入中保证任何一种连续多次洗牌的方式都可以用这M种洗牌中的一种替换

题目分析:

很显然给出的M种洗牌实际上是M种不同的置换,然后这是一个裸的置换群问题。但是我不会啊,然后昨晚就查呀查,总算搞明白了:可以用Burnside定理解决这个问题。

置换和置换群是一套成型的理论,在自己没太搞明白的时候在这里展开介绍纯属作死……所以附上传送门:

http://wenku.baidu.com/view/9386b9d9d15abe23482f4d02.html

首先这道题输入非常人性,它保证给出的置换加上单位置换之后就是一个置换群了。但是是不可以直接用Ploya做的,因为它限定了每种颜色的数量,但是我们可以借助这个思想:群中的每个置换的不动点个数等价于给每个循环节染成相同颜色的方案数,所以只需要在外面套一个DP就可以了。

知识点:群论、DP、数学

 #include <cstdio>
#include <cstring> const int maxp = ; int sr, sb, sg, n, m, p, ans;
int inv[maxp], num[maxp], had[maxp], tp[maxp], dp[maxp][][][]; void dfs(int v, int &len){had[v] = ; if(!had[num[v]]) dfs(num[v], ++len);}
inline int pow(int n, int k)
{
int ans = ;
while(k)
{
if(k & ) ans = ans * n % p;
n = n * n % p; k >>= ;
}
return ans;
} void work(int k, int lr, int lg, int lb)
{
int ans = ;
if(lr >= tp[k])
{
if(dp[k - ][lr - tp[k]][lg][lb] == -) work(k - , lr - tp[k], lg, lb);
ans += dp[k - ][lr - tp[k]][lg][lb];
}
if(lg >= tp[k])
{
if(dp[k - ][lr][lg - tp[k]][lb] == -) work(k - , lr, lg - tp[k], lb);
ans += dp[k - ][lr][lg - tp[k]][lb];
}
if(lb >= tp[k])
{
if(dp[k - ][lr][lg][lb - tp[k]] == -) work(k - , lr, lg, lb - tp[k]);
ans += dp[k - ][lr][lg][lb - tp[k]];
}
dp[k][lr][lg][lb] = ans % p;
} int main()
{
scanf("%d%d%d%d%d", &sr, &sb, &sg, &m, &p); n = sr + sb + sg; inv[] = ;
for(int i = ; i < p; ++i) inv[i] = inv[p % i] * (p - p / i) % p; for(int j = ; j <= n; ++j) num[j] = j;
for(int i = ; i <= n; ++i) tp[i] = ;
memset(dp, 0xFF, sizeof dp);
dp[][][][] = ;
work(n, sr, sg, sb);
ans = dp[n][sr][sg][sb] % p; for(int i = ; i <= m; ++i)
{
for(int j = ; j <= n; ++j) scanf("%d", &num[j]);
memset(had, , sizeof had);
int len = , tk;
for(int j = ; j <= n; ++j) if(!had[j]){dfs(j, tk = ); tp[++len] = tk;}
memset(dp, 0xFF, sizeof dp);
dp[][][][] = ;
work(len, sr, sg, sb);
ans = (ans + dp[len][sr][sg][sb]) % p;
} ans = ans * inv[m + ] % p;
printf("%d\n", ans);
return ;
}

Cards

BZOJ1005 明明的烦恼:

题目大意:

有一棵n个点的无根树,给出每个点的度数d[i](可能不确定),然后求满足条件的树的个数

题目分析:

应用组合数学里的prufer序列求解。(HNOI2008都是什么蛋疼知识点……完全是让我这样啥都不会的弱渣得不到分啊)

prufer序列的定义是这样的:每次将一棵树编号最大的叶子节点消去,并将与它相邻的节点编号加入到序列中,直至树中只剩下两个点。有定理是说,prufer序列和无根树是一一对应的。然后问题就转化成为了求长度为n-2,且第i个数出现了d[i]-1次的prufer序列的个数。先考虑所有已经确定度数的点,设共有m个,每个点需要占用d[i]-1个格子,那么设剩余格子数为\(now\),那么这个点就有\({now \choose d[i]-1}\)种选法,那么总的选择方案数就是\[tp=\prod_{i=1}^{m} {n - 2-  \sum_{j=1}^{i-1} (d[j] - 1) \choose d[i] - 1}\]

接下来我们去考虑一下那些度数不确定的点,共有n-m个。现在序列中的空格数是\[n-2-\sum_{i=1}^{m} (d[i] - 1)\]其实每个空格都可能是剩下的这些点的任何一个,然后总共有\[w=(n-2-\sum_{i=1}^{m} (d[i] - 1))^{n-m}\]种方案,所以总的答案数就是\(ans=tp*w\)种,然后直接算就可以了。

需要注意的是,直接乘啊除啊会爆的,但是高精度就蛋疼了,所以推荐质因数分解之后做,然后最后输出结果的时候就只有高精度加和乘了,十分方便

知识点:组合数学、数论

 //date 20140620
#include <cstdio>
#include <cstring> const int maxn = ; int n, nprime, sum, k, p;
int prime[maxn], sgn[maxn], mt[maxn], d[maxn]; inline int innew(int &a, int b){if(a < b){a = b; return ;} return ;}
struct bigInteger
{
int num[];
int len;
bigInteger(){num[len = ] = ;}
void print()
{
for(int i = len; i; --i) printf("%d", num[i]);
printf("\n");
}
void read(char s[])
{
len = strlen(s + );
for(int i = len; i; --i) num[i] = s[len - i + ]- '';
}
}ans; inline bigInteger operator*= (bigInteger &A, int B)
{
for(int i = ; i <= A.len; ++i) A.num[i] *= B;
for(int pos = ; pos < A.len || A.num[pos] >= ; innew(A.len, ++pos))
{
A.num[pos + ] += A.num[pos] / ;
A.num[pos] %= ;
}
return A;
} inline void get_prime()
{
sgn[] = ;
for(int i = ; i <= n; ++i)
{
if(!sgn[i]) prime[++nprime] = i;
for(int j = ; j <= nprime && prime[j] * i <= n; ++j)
{
sgn[prime[j] * i] = ;
if(i % prime[j] == ) break;
}
}
} inline void change(int val, int k)
{
for(int i = ; prime[i] <= val && i <= nprime; ++i)
while(val % prime[i] == ){mt[i] += k; val /= prime[i];}
} int main()
{
scanf("%d", &n);
get_prime(); for(int i = ; i <= n; ++i)
{
scanf("%d", &d[i]);
if(n != && d[i] == ){printf("0\n"); return ;}
if(d[i] != -) sum += d[i] - ;
else ++p;
} if(sum > n - || (sum < n - && p == )) {printf("0\n"); return ;}
if(n == ){printf("1\n"); return ;} int now = n - ;
for(int i = ; i <= n; ++i) if(d[i] != -)
{
for(int j = ; j < d[i]; ++j) {change(now - j + , ); change(j, -); }
now -= d[i] - ;
if(now < ){printf("0\n"); break;}
} change(p, now); for(int i = ; i <= nprime; ++i)
for(int j = ; j <= mt[i]; ++j)
ans *= prime[i]; ans.print();
return ;
}

tree

BZOJ1006 神奇的国度:

题目大意:

有一个n个点的弦图,求弦图的最小染色

题目分析:

一定是我太弱了!弦图的相关知识参考CDQ姐姐的讲稿就可以了,里面介绍了这道题。

传送门:http://wenku.baidu.com/link?url=HlsEeVgc12pwzyJ7s4q0ccn8waNBRkQIB3RU2um56IkimHrAZ7TkIwaOZJ0uK_xcKSeF4zpOs4UbC_SGcW-hNaqrmXg_4neG9I86wh4RnAS

先用最大势算法求出完美消除序列,然后贪心染色即可了。一开始不太明白最大势怎么做到线性的,一直以为是跟堆优化Prim一样带个log,后来发现我sb了(貌似一直是?)。其实就是做一个双向链表。链表的表头是这一串点的势,然后取最大和删除节点、修改某个节点的势就变成均摊O(1)的了,总复杂度是O(n),详见代码

知识点:图论

 //date20140622
#include <cstdio>
#include <cstring> const int maxn = ;
const int maxm = ; inline int getint()
{
int ans(); char w = getchar();
while(w < '' || '' < w) w = getchar();
while('' <= w && w <= ''){ans = ans * + w - ''; w = getchar();}
return ans;
}
inline int innew(int &a, int b){if(a < b){a = b; return ;} return ;} struct edge{int v, next;} E[maxm];
int a[maxn], nedge;
inline void add(int u, int v){E[++nedge].v = v; E[nedge].next = a[u]; a[u] = nedge;} struct info{int last, v, next, bt;}MEOW[maxn];
struct link{int v, next;} head[maxn]; int nval, n, m;
int lab[maxn], order[maxn], col[maxn], ncol, used[maxn]; int main()
{
n = getint(); m = getint();
for(int i = ; i <= m; ++i)
{
int x = getint(), y = getint();
add(x, y); add(y, x);
} head[nval = ].v = ;
for(int i = ; i < n; ++i) head[i].next = i + ;
for(int i = ; i <= n; ++i) { MEOW[i].bt = ; MEOW[i].v = i; MEOW[i].last = i - ; MEOW[i].next = i + ;} int now;
MEOW[now = n].next = ; for(int i = n; i; --i)
{
order[lab[now] = i] = now;
if(!MEOW[now].last)
{
head[MEOW[now].bt].v = MEOW[now].next;
if(MEOW[now].next) MEOW[MEOW[now].next].last = ;
}
else {MEOW[MEOW[now].last].next = MEOW[now].next; if(MEOW[now].next) MEOW[MEOW[now].next].last = MEOW[now].last;} for(int j = a[now]; j; j = E[j].next) if(!lab[E[j].v])
{
if(MEOW[E[j].v].bt + > nval) ++nval; if(!MEOW[E[j].v].last)
{
head[MEOW[E[j].v].bt].v = MEOW[E[j].v].next;
if(MEOW[E[j].v].next) MEOW[MEOW[E[j].v].next].last = ;
}else{
MEOW[MEOW[E[j].v].last].next = MEOW[E[j].v].next;
if(MEOW[E[j].v].next)MEOW[MEOW[E[j].v].next].last = MEOW[E[j].v].last;
} MEOW[E[j].v].last = ;
MEOW[E[j].v].next = head[MEOW[E[j].v].bt + ].v;
if(head[MEOW[E[j].v].bt + ].v) MEOW[head[MEOW[E[j].v].bt + ].v].last = E[j].v;
head[++MEOW[E[j].v].bt].v = E[j].v;
}
while(!head[nval].v) --nval;
now = head[nval].v;
} for(int i = n; i; --i)
{
int ran = ;
for(int j = ; j <= ncol; ++j) used[j] = ;
for(int j = a[order[i]]; j; j = E[j].next) used[col[E[j].v]] = ;
for(int j = ; j <= ncol; ++j) if(!used[j]) ran = j;
if(!ran) col[order[i]] = ++ncol; else col[order[i]] = ran;
}
printf("%d\n", ncol);
return ;
}

country

BZOJ1007 水平可见直线:

题目大意:

给出坐标系中n条直线(不平行于y轴),求有那些是在y无穷大处往下看可见的

题目分析:

感觉这道题还算正常……首先非常显然的一个结论是如果两条直线的斜率相同那么纵截距小的一定会被大的挡上。所以这样可以筛除掉一些直线使得剩下的直线两两有交点。

剩下的直线按照斜率从小到大,然后用一个栈维护所有可见的直线。首先斜率最大的直线和斜率最小的直线都一定是可见的。按照斜率的顺序向栈中添加直线,那么新添加的直线一定不会被之前的直线挡上,但是它可能挡上它前面的。画一下图可以看出它挡住原先栈顶直线(top)等价与它与栈顶的交点的横坐标大于等于它与top-1号直线的交点的横坐标。然后最后栈中剩下的直线就是答案了

建议求交点横坐标的时候乘上来直接用整数比,实数的话精度啊速度啊都挺烦人的。

知识点:栈、几何(算么?)、排序

 

 //date 20140622
#include <cstdio>
#include <cstring>
#include <algorithm> const int maxn = ; inline int getint()
{
int ans(), sgn(); char w = getchar();
while(w < '' || '' < w) {sgn = w == '-'; w = getchar();}
while('' <= w && w <= ''){ans = ans * + w - ''; w = getchar();}
return sgn ? -ans : ans;
} int n;
struct line{int a, b, lab;} tmp[maxn], L[maxn];
inline int operator<(line A, line B){return A.a != B.a ? A.a < B.a : A.b > B.b;}
int stack[maxn], stop, avl[maxn]; inline int check(int now, int last, int llast)
{
return (long long)(L[now].b - L[last].b) * (L[llast].a - L[now].a) <= (long long)(L[now].b - L[llast].b) * (L[last].a - L[now].a);
} int main()
{
n = getint();
for(int i = ; i <= n; ++i){tmp[tmp[i].lab = i].a = getint(); tmp[i].b = getint();}
std::sort(tmp + , tmp + n + ); int tn = ; L[] = tmp[];
for(int i = ; i <= n; ++i) if(tmp[i].a != tmp[i - ].a) L[++tn] = tmp[i]; for(int i = ; i <= tn; ++i)
{
while(stop > && check(i, stack[stop], stack[stop - ])) --stop;
stack[++stop] = i;
} for(int i = ; i <= stop; ++i) avl[L[stack[i]].lab] = ;
for(int i = ; i <= n; ++i) if(avl[i]) printf("%d ", i); printf("\n");
return ;
}

line

BZOJ1008 越狱:

题目大意:

*有连续编号为1...N的N个房间,每个房间关押一个犯人,有M种宗教,每个犯人可能信仰其中一种。如果相邻房间的犯人的宗教相同,就可能发生越狱,求有多少种状态可能发生越狱

题目分析:

纯水题。。转换成补集去求,考虑多少种状态不会发生越狱。枚举第一个人的宗教信仰,那么后面每个人的信仰和前一个人不相同,所以每个人有m-1种选择,所以答案就是\(m^n-m (m-1)^{n-1}\),一个快速幂就搞定了

知识点:组合数学

 //date 20140617
#include <cstdio>
#include <cstring> typedef long long LL;
const int mod = ; inline LL pow(LL n, LL k)
{
LL ans = ;
while(k)
{
if(k & ) ans = ans * n % mod;
n = n * n % mod;
k >>= ;
}
return ans;
} LL n, m; int main()
{
scanf("%lld%lld", &m, &n);
LL ans = pow(m, n) - m * pow(m - , n - ) % mod;
while(ans < ) ans += mod;
printf("%lld\n", ans); return ;
}

*

BZOJ1009 GT考试:

题目大意:

阿申准备报名参加GT考试,准考证号为N位数X1X2....Xn(0<=Xi<=9),他不希望准考证号上出现不吉利的数字。他的不吉利数学A1A2...Am(0<=Ai<=9)有M位,不出现是指X1X2...Xn中没有恰好一段等于A1A2...Am. A1和X1可以为0

题目分析:

显然是个dp,一开始想的是设dp[i][k]表示前i位,第i位和前面组成了不吉利数字的前k位,然后\[dp[i][0]=9\sum_{k=1}^{m}dp[i-1][k] \\ dp[i][m] = dp[i-1][m-1]\]

但是着非常奇怪跟他给出的不吉利数字没有关系了,而且这显然是不对的——转移到k号状态的不一定是k-1。例如31232,现在到了第4位3123,它可以转移到31232、31和3。非常显然这和KMP的失配函数非常像。然后就用KMP的失配函数预处理出每个状态能由哪些状态转移到,分别有几种转移方法,构建一个矩阵然后倍增就可以了

知识点:KMP,动态规划,矩阵

 //date 20140622
#include <cstdio>
#include <cstring> const int maxm = ; int n, m, p;
int num[maxm], pi[maxm];
int trans[maxm][maxm];
int tp[maxm], tmp[maxm]; inline void get_pi()
{
int k; pi[] = k = ;
for(int i = ; i <= m; ++i)
{
while(k && num[k + ] != num[i]) k = pi[k];
pi[i] = k += (num[k + ] == num[i]);
}
} struct Mat
{
int num[maxm][maxm];
int *operator[](int x){return num[x];}
void clear(){memset(num, , sizeof num);}
void unit(){clear(); for(int i = ; i <= m; ++i) num[i][i] = ;}
}Ans; inline Mat operator *(Mat &A, Mat &B)
{
Mat ans; ans.clear();
for(int i = ; i <= m; ++i)
for(int k = ; k <= m; ++k)
for(int j = ; j <= m; ++j)
ans[i][j] = (ans[i][j] + A[i][k] * B[k][j]) % p;
return ans;
} inline Mat pow(Mat n, int k)
{
Mat ans; ans.unit();
while(k)
{
if(k & ) ans = ans * n;
n = n * n; k >>= ;
}
return ans;
} int main()
{
scanf("%d%d%d\n", &n, &m, &p);
for(int i = ; i <= m; ++i) num[i] = getchar() - '';
get_pi(); for(int i = ; i < m; ++i) trans[i][] = ;
for(int i = ; i < m; ++i)
for(int k = ; k <= ; ++k)
{
int w = i; while(w && num[w + ] != k) w = pi[w];
if(num[w + ] == k) {trans[i][w + ]++; trans[i][]--;}
} for(int i = ; i <= m; ++i) for(int j = ; j <= m; ++j) Ans[i][j] = trans[i][j]; Ans = pow(Ans, n - );
tp[] = ; tp[] = ;
for(int i = ; i <= m; ++i) for(int j = ; j <= m; ++j) tmp[i] = (tmp[i] + tp[j] * Ans[j][i]) % p; int ans = ;
for(int i = ; i < m; ++i) ans = (ans + tmp[i]) % p;
printf("%d\n", ans);
return ;
}

GT

BZOJ1010 玩具装箱:

题目大意:

P教授有编号为1...N的N件玩具,第i件玩具经过压缩后变成一维长度为$C_i$.为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为 \(x=j-i+\sum_{k=i}^{j} C_k \) 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为\((X-L)^2\).其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小.

题目分析:

由于给出的玩具的顺序固定,显然还是个dp。看这模式就像斜率优化(一套题考两道DP优化什么心态?)。然后这题是之前做的推式子的草稿纸找不到了懒得重推了所以抱歉这题不多说了

知识点:动态规划

 //date 20140419
#include <cstdio>
#include <cstring> inline long long getint()
{
long long ans(); char w = getchar();
while(w < '' || '' < w) w = getchar();
while('' <= w && w <= '')
{
ans = ans * + w - '';
w = getchar();
}
return ans;
}
inline long long denew(long long &a, long long b){if(a > b){a = b; return ; }return ;} const long long maxn = ; long long n, l;
long long c[maxn], s[maxn];
long long f[maxn], y[maxn], x[maxn], q[maxn], w[maxn], st, ed; int main()
{
n = getint(); l = getint();
for(long long i = ; i <= n; ++i)s[i] = s[i - ] + (c[i] = getint()); q[st = ed = ] = ; for(int i = ; i <= n; ++i)w[i] = (i + + s[i] - l) << ; for(long long i = ; i <= n; ++i)
{
f[i] = (s[i] + i - - l) * (s[i] + i - - l);
while(st < ed && (y[q[st + ]] - y[q[st]] < w[i] * (x[q[st + ]] - x[q[st]])))++st;
denew(f[i], f[q[st]] + (i - q[st] - + s[i] - s[q[st]] - l) * (i - q[st] - + s[i] - s[q[st]] - l));
x[i] = i + s[i]; y[i] = f[i] + x[i] * x[i];
while(st < ed && (y[i] - y[q[ed]]) * (x[q[ed]] - x[q[ed - ]]) < (y[q[ed]] - y[q[ed - ]]) * (x[i] - x[q[ed]]))--ed;
q[++ed] = i;
} printf("%lld\n", f[n]);
return ;
}

toy

BZOJ1011 遥远的行星:

题目大意:

直线上N颗行星,X=i处有行星i,行星J受到行星I的作用力,当且仅当\(i\le Aj\).此时J受到作用力的大小为 \(F_{i->j}=\frac{M_i\times M_j}{j-i}\) 其中A为很小的常量,故直观上说每颗行星都只受到距离遥远的行星的作用。请计算每颗行星的受力,只要结果的相对误差不超过5%即可.

题目分析:

这题本来感觉挺难的,但是hq教我用逗比的乱搞过得……结果的相对误差不超过5%是一个逗比要求,我们就可以把误差范围内的东西看作是一样的然后分块部分和乱搞就可以了。

具体是这样的:考虑每个点i的受力范围1~j,其中$j=\lfloor Ai \rfloor$,设$k[x] = \frac{1}{i-x}$(就是原题中的分母)然后观察这个范围中的一段l~r,如果$k[l] \ge \frac{19}{21}k[r]$就把l~r这一段都视作k[r],然后直接分块求和就行了。

用long long好似会WA,全局double最好

知识点:乱搞

 //date 20140622
#include <cstdio>
#include <cstring>
#include <cmath> const int maxn = ; int n, num[maxn];
double sum[maxn], A, ans[maxn]; int main()
{
scanf("%d%lf", &n, &A);
for(int i = ; i <= n; ++i) scanf("%d", &num[i]);
for(int i = ; i <= n; ++i) sum[i] = sum[i - ] + num[i]; for(int i = ; i <= n; ++i)
{
for(int l, r = floor(A * i); r; r = l - )
{
l = ceil(21.0 / 19.0 * (double) r - 2.0 / 19.0 * (double) i);
ans[i] += (sum[r] - sum[l - ]) * 20.0 / 21.0 * (1.0 / (double)(i - r));
if(l < ) break;
}
ans[i] *= (double) num[i];
} for(int i = ; i <= n; ++i) printf("%.8lf\n", ans[i]); return ;
}

planet

感觉这套题,说难吧……代码没有超过90行的,说简单吧……好多东西我还头一次见

这个时候了,不会的东西再不去赶紧学纯属找死……继续加油!