2025.2.9CW集训综合训练
A:
题意:
有\(n\)个点,每个点具有一个权值\(a_i\),同时具有一个初始权值\(first\),每次只能选权值比当前总权值小的点,选择后将其权值贡献给总权值,要求每个点恰好访问一次,求有多少种不同的选择方案,对\(10^9 + 9\)取模
思路:
自然的,我们可以暴力搜索。考虑优化,发现求的是方案数,而不是具体的方案,而且每一个\(a_i\)都很小,所以我们考虑开一个大小为\(50\)的桶去存相同大小权值的数量,并且注意到,\(0\)这一权值在任何时间点都不会使答案不合法,所以先不统计\(0\)的个数,最后再排列组合乘回去就好了。
那么考虑搜索如何优化,首先记忆化是肯定的,但如果常规的记忆化肯定会炸,这里给出一个新的思路——hash记忆化,即对桶数组的数量排列方案同字符串一样进制哈希,这样可以大大简化枚举排列的过程,同时若总和已经大于\(50\),直接用排列公式常数计算,然后返回,也有非常不错的时间优化。
说的可能比较抽象,看代码。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<unordered_map>
#include<algorithm>
#define int long long
#define ull unsigned long long
const int N = 1e5 + 10,MOD = 1e9 + 7;
int n,a[55],fac[N],num,fir_st;
std::unordered_map<ull,int> dp;
const ull P = 131;
void init(){
fac[0] = 1;
for(int i = 1;i < N;++i) fac[i] = fac[i - 1] * i % MOD;
}
int dfs(int rest,int sum){
if(!rest) return 1;
if(sum >= 50) return fac[rest];
ull change = 0;
for(int i = 50;i;--i) change = change * P + a[i];
if(dp[change]) return dp[change];
int res = 0;
for(int i = 1;i <= sum;++i){
if(!a[i]) continue;
--a[i];
res = (res + (a[i] + 1) * dfs(rest - 1,sum + i) % MOD) % MOD;
++a[i];
}
return dp[change] = res;
}
signed main(){
init();
std::cin >> n >> fir_st;
for(int i = 1;i <= n;++i) {
int x;
std::cin >> x;
if(x){
++a[x];
++num;
}
}
int ans = dfs(num,fir_st);
for(int i = num + 1;i <= n;++i){
ans = (ans * i) % MOD;
}
std::cout << ans;
return 0;
}
B:
题意:
给定一个数\(k\)和一个位数\(b\),求:在区间\([0,2^b-1]\)范围内的整数,是\(k\)的倍数的整数在二进制表示下总共加起来有多少个\(1\)。
思路:
这不是直接秒了,一眼数位dp,唯一要提的一点就是,不能真的直接存现在的值是多大,而是用\(k\)的倍数这一限制,通过取模运算的性质减少空间消耗。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
const int MOD = 1e9 + 9;
const int POS = 130,K_REST = 1000 + 10;
int dp[POS][K_REST][POS];
int k,b;
ll Dp(int pos,int num,int sum,bool limit){
if(pos == -1) return num ? 0 : sum;
if(!limit && dp[pos][num][sum] != -1) return dp[pos][num][sum];
ll ans = 0;
for(int i = 0;i <= 1;++i){
ans += Dp(pos - 1,(num * 2 + i) % k,sum + i,limit && i);
ans %= MOD;
}
if(!limit) dp[pos][num][sum] = ans;
return ans;
}
ll Get(){
return Dp(b - 1,0,0,true);//令b-1更符合二进制的特征
}
int main(){
std::cin >> k >> b;
memset(dp,-1,sizeof(dp));
std::cout << Get() << '\n';
return 0;
}
C:
题意 + 思路:
太懒了,感觉这一篇写的挺好:优秀的TJ
D:
题意:
思路:
F:(此题输出其中一个满足题意的有效串即可)
题意:
给定一个字符串集\(D,(|D|=d)\),给定一个数\(t\),表示所有字符串中的字符都在英文小写字母表中的\([1,t]\)范围内。现在我们给定一个\(k\),限制一个字符串是有效的的条件。
我们称一个字符串是有效的,当且仅当其任意长度为\(k\)的子串都是\(D\)中的一个元素。
(注:字串是连续的子序列)
那么再给出一个矩阵\(P\)和一个数\(n\),\(P_{i,j}(0<i \leq n,0<j\leq t)\)表示最终答案串里某一个字符的出现概率,我们希望,最终的答案串是一个长度为\(n\)的有效的串,且其最终概率,即:\(\prod_{i=1}^nP_{i,c_{i}}\)最大,且题目保证:\(\sum_{c=1}^tP_{i,c}=1\)
思路:
首先有一个逻辑,我们发现对于一段长度为\(k\)的有效子串,假想有一个窗口框住了他,那么当这个窗口向后移一位之后,这个窗口框住的子串同样是长度为\(k\)的在\(D\)中的子串,所以,最终答案的每一位,都一定是\(D\)中的一个元素串的一个结尾。
这个逻辑看上去很好理解,但是很关键,这提醒我们可以使用动态规划去求解这一问题,现在,我们给\(D\)中字符串编号,并且定义状态\(dp_{i,j}\)表示在答案串中第\(i\)位填入第j个\(D\)中元素串结尾的最大概率,同时定义一个辅助数组\(go_{i,j}\)表示编号为\(i\)的元素串去掉开头,添上一个字符\(j\)后会变成编号为多少的元素串(前提是可以变化),据此,我们就可以枚举字符串\(c\)并进行转移
给出转移,定义\(next\)等于每一次的\(go_{j,c}\),那么:
最后在根据转移回溯反推最优有效串就好了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#include<cstdio>
#define double long double
const int N = 1000 + 5,D = 200 + 5,T = 30;
double p[N][T];
std::string str[D];
std::map<std::string,int> hash;//给字符串编号
int go[D][T],last[N][D],n,d,k,t;//last给回溯的时候方便查询
double dp[N][D];
int main(){
freopen("decoding.in","r",stdin);
freopen("decoding.out","w",stdout);
std::cin >> d >> k >> t;
for(int i = 0;i < d;++i) {
std::cin >> str[i];
hash[str[i]] = i;
}
memset(go,-1,sizeof(go));
for(int i = 0;i < d;++i){
std::string tmp = str[i].substr(1);
for(int j = 0;j < t;++j){
tmp += (char)('a' + j);
if(hash.find(tmp) != hash.end()) go[i][j] = hash[tmp];
tmp.pop_back();
}
}
std::cin >> n;
for(int i = 0;i < n;++i)
for(int j = 0;j < t;++j)
std::cin >> p[i][j];
//dp部分:
//1.初始化
for(int i = 0;i < d;++i){
dp[k - 1][i] = 1.0;
for(int j = 0;j < k;++j) dp[k - 1][i] *= p[j][str[i][j] - 'a'];
}
for(int i = k - 1;i < n - 1;++i)
for(int j = 0;j < d;++j)
for(int c = 0;c < t;++c){
if(go[j][c] == -1) continue;
int nxt = go[j][c];
if(dp[i + 1][nxt] < dp[i][j] * p[i + 1][c]){
dp[i + 1][nxt] = dp[i][j] * p[i + 1][c];
last[i + 1][nxt] = j;
}
}
double tmp = 0;
int now = -1;//回溯用
for(int i = 0;i < d;++i){
if(dp[n - 1][i] > tmp){
tmp = dp[n - 1][i];
now = i;
}
}
if(tmp <= 0){
puts("---");
return 0;
}
int cur = n - 1;
std::string res;
while(cur > k - 1){
res = str[now].back() + res;
now = last[cur][now];
--cur;
}
res = str[now] + res;
std::cout << res;
return 0;
}

浙公网安备 33010602011771号