4 Commits

Author SHA1 Message Date
CakeCN
ede89cef53 fix load_full_markdown_file import bug 2025-11-18 22:15:45 +08:00
CakeCN
0fd548edcd save_load_learned 2025-11-17 23:14:55 +08:00
553a7724b3 allow clear chat log 2025-11-17 16:35:26 +08:00
Cai
0852e12118 cdn fix more 2025-11-17 14:04:52 +08:00
30 changed files with 4119 additions and 484 deletions

3187
Html/.log

File diff suppressed because one or more lines are too long

View File

@@ -48,4 +48,5 @@ class Config:
}
ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url']
ASE_ENGINE_URL_TOKEN = GLOBAL_CONFIG['ASE_ENGINE']['url_token']
ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace']

View File

@@ -1,6 +1,8 @@
# myapp/services/course_service.py
import re
from typing import Dict, List
import requests
import json
from typing import Dict, List, Optional
from flask import current_app
from urllib.parse import urlparse
from datetime import datetime
@@ -450,10 +452,10 @@ def _default_markdowns(lesson_name: str):
## 阶段一:示例阶段
### 步骤A
请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
### 步骤B
请基于“步骤B”的教学目标与材料,生成指导性提示词。
请基于"步骤B"的教学目标与材料,生成指导性提示词。
"""
score_prompt_md = f"""# {lesson_name}
@@ -463,8 +465,134 @@ def _default_markdowns(lesson_name: str):
## 阶段一:示例阶段
### 步骤A
对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
### 步骤B
同上,对“步骤B”输出进行评分与反馈。
同上,对"步骤B"输出进行评分与反馈。
"""
return lesson_md, prompt_md, score_prompt_md
def save_learning_progress(user_uuid: str, chatmanager, user_id: str) -> bool:
"""
保存用户学习进度到MongoDB
参数:
- user_uuid: 用户UUID
- chatmanager: ChatManager实例包含学习进度数据
- user_id: 用户ID
返回:
- bool: 保存是否成功
"""
try:
# 获取MongoDB连接
mongo = current_app.extensions["mongo"]
# 获取MultiAgents框架中的对话列表
dialog_list = get_multiagents_dialog_list(user_id)
# 构建学习进度数据
progress_data = {
"user_uuid": user_uuid,
"user_id": user_id,
"material_id": chatmanager.material_id,
"chapter_name": chatmanager.chapter_name,
"lesson_name": chatmanager.lesson_name,
"chapter_chain_now": chatmanager.chapter_chain_now,
"chat_historys": chatmanager.chat_historys,
"scores": chatmanager.scores,
"multiagents_dialogs": dialog_list,
"updated_at": datetime.now()
}
# 保存到MongoDB使用upsert操作如果存在则更新不存在则插入
mongo.db.learning_progress.update_one(
{
"user_uuid": user_uuid,
"material_id": chatmanager.material_id,
"chapter_name": chatmanager.chapter_name,
"lesson_name": chatmanager.lesson_name
},
{"$set": progress_data},
upsert=True
)
current_app.logger.info(f"学习进度保存成功: user_uuid={user_uuid}, lesson={chatmanager.lesson_name}")
return True
except Exception as e:
current_app.logger.error(f"保存学习进度失败: {str(e)}")
return False
def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
"""
从MultiAgents框架获取用户对话列表
参数:
- user_id: 用户ID
返回:
- List[Dict]: 对话列表
"""
try:
# 从配置中获取ASE引擎URL和token
config = current_app.config
url_token = config.get("ASE_ENGINE_URL_TOKEN", "")
# 构建请求URL
base_url = config.get("ASE_ENGINE_URL", "")
api_url = f"{base_url}/api/sync/download"
# 构建请求参数
payload = {
"namespace_url": url_token, # 使用配置中的token作为namespace_url
"user_id": user_id
}
# 发送POST请求
response = requests.post(
api_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
# 检查响应
if response.status_code == 200:
return response.json()
else:
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")
return []
except Exception as e:
current_app.logger.error(f"获取MultiAgents对话列表异常: {str(e)}")
return []
def load_learning_progress(user_uuid: str, material_id: str, chapter_name: str, lesson_name: str) -> Optional[Dict]:
"""
加载用户学习进度
参数:
- user_uuid: 用户UUID
- material_id: 课程ID
- chapter_name: 章节名
- lesson_name: 课时名
返回:
- Optional[Dict]: 学习进度数据如果不存在则返回None
"""
try:
mongo = current_app.extensions["mongo"]
progress = mongo.db.learning_progress.find_one({
"user_uuid": user_uuid,
"material_id": material_id,
"chapter_name": chapter_name,
"lesson_name": lesson_name
})
if progress:
# 转换ObjectId为字符串
if "_id" in progress:
progress["_id"] = str(progress["_id"])
return progress
return None
except Exception as e:
current_app.logger.error(f"加载学习进度失败: {str(e)}")
return None

View File

@@ -7,7 +7,8 @@ from flask_socketio import Namespace, join_room, leave_room, emit
from ..function_tools import judge
from ..function_tools import next_chapter
from ..services.backboard_service import realtime_response
from ..services.markdown_service import load_full_markdown_file
from ..services.markdown_service import load_full_markdown_file
from ..services.course_service import save_learning_progress
from ..extension_ase.ase_client import HSAEngineClient
from ..services.user_service import get_or_load_current_user
@@ -71,8 +72,32 @@ class AgentNamespace(Namespace):
chatmanager = user_uuid2chatmanager[user_uuid]
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
chatmanager.chat_historys = []
chatmanager.scores = []
# 尝试加载用户学习进度
from ..services.course_service import load_learning_progress
progress = load_learning_progress(user_uuid, course_id, chapter_name, lesson_name)
need_skip_chapters = 0
if progress:
# 恢复学习进度数据
if 'scores' in progress:
# 根据scores的数量确定需要跳过的章节数
need_skip_chapters = len(progress['scores'])
chatmanager.scores = progress['scores']
print(f"恢复用户学习进度: {user_uuid}, 已完成 {need_skip_chapters} 个章节")
# 恢复聊天历史
if 'chat_historys' in progress:
chatmanager.chat_historys = progress['chat_historys']
else:
chatmanager.chat_historys = []
# 恢复当前章节链
if 'chapter_chain_now' in progress:
chatmanager.chapter_chain_now = progress['chapter_chain_now']
else:
chatmanager.chat_historys = []
chatmanager.scores = []
fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name)
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
@@ -80,6 +105,10 @@ class AgentNamespace(Namespace):
backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
chatmanager.next_chapter()
# 如果有保存的进度,需要跳过已经完成的章节
for _ in range(need_skip_chapters):
chatmanager.next_chapter()
self.chatmanager = chatmanager
self.ase_client = user_uuid2ase_client[user_uuid]
user_uuid2ase_client[user_uuid].register_on_connect_entry(
@@ -182,6 +211,7 @@ class AgentNamespace(Namespace):
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
ase_client.chatmanager.load_next_chapter()
ase_client.send_text("chapter-start", "")
ase_client.send_text("clear","")
namespace_entry.emit('message', "Multi-Agents服务已连接", room=init_data['room'], namespace='/agent')
#接受消息回来直接让message监听发送
@@ -220,7 +250,7 @@ class AgentNamespace(Namespace):
if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
else: chatmanager.scores.append(data.get('data', None))
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.material_id, chatmanager.chapter_name, chatmanager.lesson_name)
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
chatmanager.load_next_chapter()
time.sleep(0.5)
chatmanager.ase_client.send_text("chapter-start", "")
@@ -240,10 +270,32 @@ class AgentNamespace(Namespace):
def on_disconnect(self,data):
print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
ex = current_app.extensions
uuid = session.get('user_uuid')
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
user_uuid2chatmanager[uuid].disconnect(uuid)
user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {})
# 保存学习进度到MongoDB
try:
# 获取用户ID
uuid2username = ex.get("uuid2username", {})
user_id = uuid2username.get(uuid, "unknown")
# 调用保存学习进度功能
chatmanager = user_uuid2chatmanager.get(uuid)
if chatmanager:
save_result = save_learning_progress(uuid, chatmanager, user_id)
print(f"保存学习进度结果: {save_result}, user_uuid={uuid}")
except Exception as e:
print(f"保存学习进度时发生异常: {str(e)}")
# 执行原有的断开连接逻辑
if uuid in user_uuid2chatmanager:
user_uuid2chatmanager[uuid].disconnect(uuid)
# 离开房间
if uuid:
leave_room(uuid, namespace='/agent')
print("VSCode client disconnected")
print("Disconnect reason:"+str(data))

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>新7周课时</h1>
<h2>阶段一:示例阶段</h2>
<h3>步骤A</h3>
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
<h3>步骤B</h3>
<p>继续补充该阶段的其它步骤内容。</p>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>基于比较的排序</h1>
<h2>基于比较的排序</h2>
<h3>优先队列的原理</h3>
<p><strong>优先队列Priority Queue</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
<br>
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
<br><br>
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
<br><br>
<strong>Heap结构</strong>是实现优先队列的首选。</p>
<h3>数组模拟堆实现的优先队列</h3>
<h4>堆结构与取存原理</h4>
<p>Heap本质上是一个完全二叉树结构
(当然也可以是多叉树,但没有必要)
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1大根堆的二叉树结构图示" /></p>
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
<h5>取数</h5>
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
但是将这个数取出后还不能结束,因为需要维护堆的性质。
<br>
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
<br></p>
<h5>存数</h5>
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
<h4>数组模拟堆</h4>
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2左边是堆的数组表示右边是其对应的二叉树" />
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
<p>数组下标从1开始任意一个下标i其左右儿子下标刚好就是&quot;i*2&quot;和“i*2+1”。这样在后续代码实现时代码写起来就会简单很多。</p>
<h6>建堆</h6>
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
对于一个元素随机的数组只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
<h3>优先队列的算法实现</h3>
<h4>练习:堆操作</h4>
<p>在进行代码实现之前,做一个问题练习:
对于一个随机队列&quot;[3, 32, 6, 43, 5, 8, 0, 9]&quot;,经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
将答案告知AI教师。</p>
<h4>任务:城市事件优先调度</h4>
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
<p><br><br></p>
<h4>题目:实现堆排序</h4>
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
<br>
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
<br>
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
<br></p>
<h5>代码框架</h5>
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def max_heapify(arr, n, i):
&quot;&quot;&quot;
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
调整结点 i 使以其为根的子树成为最大堆
参数:
arr: 存储堆的数组
n: 堆的有效大小(长度)
i: 需要下滤调整的节点索引
&quot;&quot;&quot;
# TODO: 在此处实现 &quot;下滤&quot; 操作,将 arr[i] 下沉到正确位置
def build_max_heap(arr):
&quot;&quot;&quot;
将无序数组原地建成最大堆,从后往前进行下滤
参数:
arr: 待调整的数组
&quot;&quot;&quot;
# TODO: 调用 max_heapify 将 arr 调整为堆
def heap_sort(arr):
&quot;&quot;&quot;
利用堆排序算法排序数组(降序)
参数:
arr: 待排序数组
返回:
排序后的数组(从大到小)
&quot;&quot;&quot;
# TODO: 完成堆排序的实现
n = len(arr)
# 1. 原地建堆
build_max_heap(arr)
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
#性能测试:对比不同规模输入的堆排序用时
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data.copy())
end = time.perf_counter_ns()
return (end - start) / 10**6 # 毫秒
sizes = [1000, 5000, 10000]
print(&quot;堆排序性能测试:&quot;)
for n in sizes:
data = [random.randint(0, 1000000) for _ in range(n)]
t = measure(heap_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<h5>实验结果分析</h5>
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>当输入规模增大时运行时间应呈现近似线性乘以对数的增长趋势。具体来说若将输入规模扩大10倍运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
<br>
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比你会发现堆排序对规模扩大的响应增长显著缓慢得多。
<br>
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
<br>
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
<h3>比较排序的决策树模型</h3>
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
<br>
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
<p>决策树是描述比较排序过程的一种抽象模型。
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
<br>
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
<br>
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
<br>
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
<br>
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
<br>
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
<br>
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符对三个无任何特殊性质的数进行排序最少需要3次比较才能确定它们的正确顺序。</p>
</body>
</html>

View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>非比较型排序</h1>
<h2>非比较型排序</h2>
<h3>基数排序原理</h3>
<h4>排序算法复杂度背景</h4>
<p>基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界
特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序</p>
<h4>基数排序核心思想</h4>
<p>将元素视为由多个 “位”digit组成
按位进行多轮排序,每轮针对一个关键位
依赖稳定的子排序算法,保证先前排好的顺序不被打乱
关键保障:<strong>排序的稳定性(相同元素相对顺序不变)</strong></p>
<h4>经典示例(十进制整数的基数排序):</h4>
<p>按最低有效位LSB→ 最高有效位MSB 逐轮排序
第一轮按个位数排序0-9 分组)
后续轮次:依次按十位数、百位数等排序</p>
<h4>实操流程:</h4>
<p>初始序列:[329, 457, 657, 839, 436]
第一轮(按个位排序):
按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)]
457 与 657 个位相同,保持原顺序)
第二轮(按十位排序):
按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)]
436 与 839 十位相同,保持第一轮后的顺序)
第三轮(按百位排序):
按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)]
436 与 457 百位相同,保持第二轮后的顺序)</p>
<p>每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。</p>
<h4>时间复杂度:</h4>
<p>每轮扫一遍序列O(n)总共轮数就是基数位数设为k。
时间复杂度为O(kn)其中k一般不太大。</p>
<h3>基数排序的实现</h3>
<p>为了加深对基数排序的理解我们通过一个编程任务来实践其实现。我们将以整数排序为例并采用从最低有效位开始的方法对数字进行排序LSD 基数排序)。</p>
<p>在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。</p>
<h4>用计数排序稳定地对基数排序</h4>
<p>每选定一个基数后需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说<strong>找到按照当前基数下,当前序列的每一个数要前往的位置</strong>
这里用“计数排序”这个算法。
具体做法如下:</p>
<ol>
<li>建一个新数组count记录当前基数的每个不同的数有多少个计数</li>
<li>从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1count[number]+=1</li>
<li>将count转变为前缀和这样就得到了每一个基数对应的原始数的最后一个在新序列中的位置</li>
<li>从后往前遍历原序列将每一个数基数上的数为number放到对应新序列count[number]-1的位置并让count[number]-=1。</li>
</ol>
<h5>练习:按位计数排序</h5>
<p>作为练习,[170, 45, 75, 90, 802, 24, 2, 66]按个位排序exp=1进行计数排序请写下count数组的计数结果2.步后和前缀和结果。并表述在第4步的利用方式从而得到序列结果。</p>
<h4>任务:实现 LSD 基数排序</h4>
<p>请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10实现并假定输入整数的最大位数为<eq>d</eq>(例如<eq>10^d</eq>数量级)。算法思路如下:</p>
<p>从最低位个位exp=1开始对数组进行计数排序Counting Sort按该位的数值对元素排序。</p>
<p>然后依次向更高一位exp=10exp=100...)进行排序,直到处理完最高位。</p>
<h4>代码框架</h4>
<p>请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def counting_sort_by_digit(arr, exp):
&quot;&quot;&quot;
对数组按某个数位进行计数排序(稳定的)。
参数:
arr: 要排序的数组
exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
例如 exp=1 表示按个位排序exp=10 表示按十位排序。
返回:
按指定位排序后的数组副本
&quot;&quot;&quot;
n = len(arr)
output = [0] * n # 存放排序结果
count = [0] * 10 # 计数数组十进制位范围0-9
# TODO: 计算每个数组元素在该位上的值,并计数
def radix_sort(arr):
&quot;&quot;&quot;
基数排序(按从低位到高位逐位排序)
参数:
arr: 待排序的整数列表
返回:
按非递减顺序排序的列表
&quot;&quot;&quot;
#TODO: 利用 counting_sort_by_digit 按位排序数组
#测试与性能分析
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data)
end = time.perf_counter_ns()
return (end - start) / 10**6 # 转换为毫秒
#生成随机测试数据
sizes = [10000, 50000, 100000]
print(&quot;基数排序性能测试:&quot;)
for n in sizes:
data = [random.randrange(0, 1000000) for _ in range(n)]
t = measure(radix_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<p>性能讨论</p>
<p>在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数<eq>d</eq>的整数,基数排序的时间复杂度为<eq>O(d \times (n + k))</eq>,其中<eq>k</eq>为每位可能的取值数量(对十进制整数而言<eq>k=10</eq>)。
<br>
<eq>d</eq>视作常数时,复杂度近似<eq>O(n)</eq>,因此输入规模扩大倍数时,运行时间应近似线性增长。
<br>
然而需注意基数排序的实际常数因子不小多次稳定排序和额外空间对中等规模数据Python实现未必比C语言内建排序更快。
<br>
更重要的是,基数排序并非万能:如果元素位数<eq>d</eq><eq>n</eq>增长(例如排序<eq>1</eq><eq>N</eq>范围的数,<eq>N</eq>约为<eq>n</eq>量级),则<eq>d = O(\log n)</eq>,此时基数排序复杂度<eq>≈O(n \log n)</eq>,并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。</p>
<h3>桶排序的原理与实现</h3>
<h4>桶排序核心定位</h4>
<p>典型的线性时间排序算法
利用输入数据的<strong>分布特性(假设大致均匀分布在某范围)</strong></p>
<h4>桶排序原理与类比</h4>
<h5>核心逻辑(三步):</h5>
<p>第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类)
第二步:对每个桶内元素单独排序(可选用任意排序算法)
第三步:按桶的顺序合并所有元素,得到有序序列
<br>
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10数据量少
桶内排序:因元素少,插入排序等简单算法足够快
合并:按桶序号收集,因 “i&lt;j 时,第 i 号桶元素<第 j 号桶元素”,天然有序
<br></p>
<h5>案例练习</h5>
<p>使用桶排序算法对以下 8 个数字进行排序
[35, 12, 48, 27, 5, 39, 18, 42]
以48为分母将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中输出每个桶里面的数字是什么。
<br><br></p>
<h4>代码实现</h4>
<p>请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
<br>
创建若干个空桶列表桶的数量可以根据数据规模n选择这里取<eq>n/10</eq>(为整数)个桶。
<br>
将每个元素按照其值乘以桶数量后的整数部分映射到对应的桶中。例如值为0.23、桶数为10时<eq>0.23 \times 10 = 2.3</eq>放入索引2号桶。
<br>
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序sorted或实现简单排序算法。</p>
<p>按桶的顺序依次合并桶内元素,得到排序后的结果。</p>
<h5>代码框架</h5>
<pre><code class="language-python">import random
import time
def bucket_sort(arr):
&quot;&quot;&quot;
桶排序适用于0到1区间的小数
参数:
arr: 介于[0,1)的浮点数列表
返回:
升序排序后的列表
&quot;&quot;&quot;
# TODO: 按原理实现桶排序
n = len(arr)
if n == 0:
return arr
# 1. 创建桶
# 2. 分配元素到各桶
# 3. 桶内排序
#简单测试与性能评估
sizes = [1000, 5000, 10000]
print(&quot;桶排序性能测试:&quot;)
for n in sizes:
#生成n个[0,1)均匀分布的小数
data = [random.random() for _ in range(n)]
start = time.perf_counter_ns()
sorted_data = bucket_sort(data)
end = time.perf_counter_ns()
elapsed = (end - start) / 10**6
#验证排序正确性
is_correct = &quot;&quot; if sorted_data == sorted(data) else &quot;×&quot;
print(f&quot;数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}&quot;)
</code></pre>
<h4>桶排序时间复杂度分析</h4>
<h5>平均情况(理想分布):</h5>
<p>前提n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素
优化:选 m 接近 n 的量级 → 桶内排序成本视为常数
结果整体期望运行时间为O(n)(线性时间)</p>
<h5>最坏情况(分布不均):</h5>
<p>极端场景:所有元素落入同一个桶
复杂度取决于桶内排序算法O (n log n) 或 O (n²)+ 分配桶的 O (n)
结果退化至O (n log n) 甚至更差,失去线性时间优势
结论:理想分布下性能出众,数据分布不均时无优势</p>
<h3>桶排序与哈希</h3>
<h4>核心相通点:映射分散思想</h4>
<p>通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
<br>
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配
哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置
这种映射也意味着都对<strong>分布均匀性的依赖</strong></p>
<h4>关键区别:映射函数的目标差异</h4>
<p>对比维度
桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分)
哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)</p>
<h4>总结</h4>
<p>桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。</p>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>分治策略进阶与主方法</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:44</p>
</blockquote>
<h2>分治策略进阶与主方法</h2>
<h3>最大子数组问题</h3>
<h4>任务:编码与分析</h4>
<h5>场景介绍</h5>
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>总增量最大的连续时段是从第8天到第11天总增量为 <code>18 + 20 - 7 + 12 = 43</code>
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
<h5>题目:最大交通流量增量</h5>
<p>你需要实现一个分治算法来解决最大子数组问题。
同时,为了对比,你也会实现一个暴力求解算法。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
<pre><code class="language-python">import time
import random
def find_maximum_subarray_brute(arr):
&quot;&quot;&quot;
使用暴力法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
#--- 本周任务:请在下方实现分治算法 ---
def find_max_crossing_subarray(arr, low, mid, high):
&quot;&quot;&quot;
找到跨越中点的最大子数组
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现寻找跨越中点的最大子数组的逻辑
# 提示: 从mid向左和向右分别扫描找到各自的最大和
def find_maximum_subarray(arr, low, high):
&quot;&quot;&quot;
使用分治法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现分治递归逻辑
# 提示: 递归基是当数组只有一个元素时
#--- 测试与对比部分 --- 以下内容无需修改
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
print(f&quot;交通流量变化数据: {traffic_changes}&quot;)
#使用分治法
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
print(f&quot;分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}&quot;)
#使用暴力法验证
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
print(f&quot;暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}&quot;)
#性能测试
large_data = [random.randint(-50, 50) for _ in range(2000)]
start_time = time.perf_counter()
find_maximum_subarray(large_data, 0, len(large_data) - 1)
dc_time = (time.perf_counter() - start_time) * 1000
print(f&quot;\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms&quot;)
start_time = time.perf_counter()
find_maximum_subarray_brute(large_data)
brute_time = (time.perf_counter() - start_time) * 1000
print(f&quot;在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms&quot;)
</code></pre>
<h3>分治经典算法:快速排序</h3>
<h4>快速排序原理</h4>
<p>快速排序Quick Sort是分治法的经典应用之一。与归并排序不同的是快速排序通过原地划分数组来完成排序无需额外的同规模辅助存储空间。它在平均情况下表现极为高效是实际应用中常用的排序算法之一。</p>
<p>快速排序的分治核心思路非常好理解对于一个未排序的数组希望分解为左区间和一个“枢轴pivot”元素和右区间其中左区间的数都小于等于枢轴右区间的都大于等于枢轴。
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
<h4>任务:编码与实验</h4>
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
代码框架
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
<pre><code>import random
import time
def quick_sort(arr):
&quot;&quot;&quot;
实现快速排序算法(固定枢轴策略)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
# TODO: 实现快速排序的分治逻辑
# 提示:选取第一个元素为枢轴,递归排序左右子数组
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6 # 转换为毫秒
#性能测试:对比有序输入和随机输入
sizes = [1000, 5000, 10000]
print(&quot;快速排序性能测试(固定枢轴):&quot;)
for n in sizes:
sorted_data = list(range(n))
random_data = [random.randint(0, 10**6) for _ in range(n)]
time_sorted = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(quick_sort, random_data)
print(f&quot;数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms&quot;)
</code></pre>
<p>完成编码并运行测试,报告时间差异。</p>
<h3>快速排序的复杂度分析</h3>
<h4>时间复杂度</h4>
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong></strong>递归深度</strong>共同决定
三种典型场景:最坏情况、平均情况、最佳情况</p>
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
平均情况:输入随机 / 等概率选枢轴
最佳情况:每次划分平分数组</p>
<h4>空间复杂度</h4>
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
<h4>分析</h4>
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
<h4>实践</h4>
<pre><code>import random
import time
def random_quick_sort(arr):
&quot;&quot;&quot;
实现随机快速排序算法(随机选择枢轴)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
def quick_sort(arr):
&quot;&quot;&quot;
你在上一章实现的快排内容
&quot;&quot;&quot;
#验证随机快排在极端有序输入下的性能
n = 10000
sorted_data = list(range(n))
time_fixed = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(random_quick_sort, sorted_data)
print(f&quot;数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms&quot;)
</code></pre>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>动态规划例题训练</h1>
<h2>动态规划例题训练</h2>
<h3>步骤A</h3>
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
<h3>步骤B</h3>
<p>继续补充该阶段的其它步骤内容。</p>
</body>
</html>

View File

@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>动态规划原理</h1>
<h2>动态规划原理</h2>
<h3>自顶而下的分治 vs. 自底向上的动态规划</h3>
<p>分治法:面对规模为 <em>n</em> 的问题,从<strong>顶层</strong>出发,将其拆为若干个更小的子问题,分别求解后<strong>合并</strong></p>
<p>动态规划DP<strong>同一子问题重复出现</strong>时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模<strong>从小到大</strong>把所有需要的子问题一次性求出来。
<br></p>
<h4>什么才算“同一”子问题</h4>
<p>与AI教师讨论如何区分“相同规模的子问题”和“同一可复用的子问题”</p>
<h3>切绳Rod Cutting问题</h3>
<p>给定一根长度为 <em>n</em> 的绳子,价格表 <code>price[i]</code> 表示长度为 <em>i</em> 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。</p>
<h4>分治法与时间复杂度</h4>
<p><code>R(n)</code> 为长度 <em>n</em> 的最大收益:</p>
<section>
<eqn> R(0) = 0, R(1)=price[1] </eqn>
</section>
<section>
<eqn> R(n) = max_{1 ≤ i ≤ n} ( price[i] + R(n - i) ) </eqn>
</section>
<p><strong>纯递归分治</strong>会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 <strong>O(n^n)</strong> 量级。</p>
<h4>记忆化(自顶向下)</h4>
<p>思想:用哈希表/数组 <code>memo[n]</code> 记录 <code>R(n)</code>。当再次需要 <code>R(n)</code> 时,直接返回已存结果,避免重复计算。</p>
<ul>
<li><strong>复杂度</strong>:时间 <strong>O(n^2)</strong>(外层 n内层枚举切第一刀 i空间 <strong>O(n)</strong></li>
</ul>
<p><strong>代码任务 A实现记忆化递归Top-Down</strong></p>
<pre><code class="language-python">def rod_cut_topdown(price: dict[int, int], n: int, memo: list[int]) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(记忆化递归)。
TODO:
1) 处理 n==02) 命中 memo 直接返回3) 枚举第一刀长度 i4) 写回 memo[n]。
&quot;&quot;&quot;
pass
</code></pre>
<h4>自底向上(反向记忆化)</h4>
<p>将规模从小到大推进:<code>dp[x]</code> 表示长度 <code>x</code> 的最优收益。
<strong>代码任务 B实现自底向上Bottom-Up</strong></p>
<pre><code class="language-python">def rod_cut_bottomup(price: dict[int, int], n: int) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(自底向上)。
TODO:
1) 初始化 dp[0]=02) for x in 1..ndp[x] = max_{1..x}( price[i] + dp[x-i] )
&quot;&quot;&quot;
&quot;&quot;&quot;
以下内容无需修改注意将你实现的rod_cut_topdown代码复制过来
&quot;&quot;&quot;
import time
import random
import signal
from functools import wraps
#超时异常定义
class TimeoutError(Exception):
pass
#超时装饰器
def timeout(seconds):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 定义超时处理函数
def handle_timeout(signum, frame):
raise TimeoutError(f&quot;Function {func.__name__} timed out after {seconds} seconds&quot;)
# 设置信号处理
signal.signal(signal.SIGALRM, handle_timeout)
signal.alarm(seconds) # 触发超时
try:
result = func(*args, **kwargs)
return result
finally:
signal.alarm(0) # 取消超时
return wrapper
return decorator
#带超时的纯递归实现(用于对比)
@timeout(1) # 1秒超时
def rod_cut_recursive(price: dict[int, int], n: int) -&gt; int:
if n == 0:
return 0
max_rev = -float('inf')
for i in range(1, n + 1):
if i in price:
max_rev = max(max_rev, price[i] + rod_cut_recursive(price, n - i))
return max_rev
#对比实验
def compare_algorithms():
# 生成测试用的价格表随机生成1到10的价格
max_length = 50
price = {i: random.randint(1, 10) for i in range(1, max_length + 1)}
print(f&quot;{'n':&lt;5} {'递归(ms)':&lt;10} {'记忆化(ms)':&lt;12} {'自底向上(ms)':&lt;15}&quot;)
print(&quot;-&quot; * 50)
# 测试n从10到50的情况
for n in range(10, 51, 5):
# 纯递归(带超时处理)
try:
start = time.time()
recursive_result = rod_cut_recursive(price, n)
recursive_time = (time.time() - start) * 1000
except TimeoutError:
recursive_result = &quot;超时&quot;
recursive_time = &quot;&gt;1000&quot;
# 记忆化递归
memo = [-1] * (n + 1)
start = time.time()
topdown_result = rod_cut_topdown(price, n, memo)
topdown_time = (time.time() - start) * 1000
# 自底向上
start = time.time()
bottomup_result = rod_cut_bottomup(price, n)
bottomup_time = (time.time() - start) * 1000
# 验证结果一致性(仅当递归未超时)
if isinstance(recursive_result, int):
assert recursive_result == topdown_result == bottomup_result, f&quot;结果不一致 for n={n}&quot;
# 输出结果
print(f&quot;{n:&lt;5} {recursive_time:&lt;10} {topdown_time:&lt;12.4f} {bottomup_time:&lt;15.4f}&quot;)
if __name__ == &quot;__main__&quot;:
compare_algorithms()
</code></pre>
<p>完成上面的代码,讨论实验对比结果。</p>
<h3>动态规划的“组成”:如何写出一个 DP</h3>
<h4><strong>四大组成</strong></h4>
<ul>
<li><strong>状态空间</strong>:用最少的下标(或维度)刻画子问题(如 <code>dp[i]</code><code>dp[i][j]</code>)。</li>
<li><strong>状态转移</strong>:写出“从更小状态到当前状态”的递推/转移式。</li>
<li><strong>边界条件</strong>:初始已知的最小规模答案(如 <code>dp[0]=0</code>)。</li>
<li><strong>解的恢复(部分题目可能不需要)</strong>:若需输出方案/路径,记录子问题选择来源(从那个子问题的答案转移而来)(如 <code>choice[i][j]</code>)。</li>
</ul>
<h4><strong>两大性质</strong></h4>
<ul>
<li><strong>最优子结构</strong>:全局最优由若干子问题的最优解组合而成;</li>
<li><strong>重复子问题</strong>:不同路径会遇到同一个(或等价的)子问题。</li>
</ul>
<h3>例题矩阵连乘Matrix-Chain Multiplication, MCM</h3>
<h4>问题描述</h4>
<p>矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
即:
矩阵 A维度为 m × k共 m 行、k 列)
矩阵 B维度为 k × n共 k 行、n 列)
矩阵 C = A×B维度为 m × n
产生标量乘法次数为 <code>m×n×k</code>
给定矩阵链 <code>A₁A₂…Aₖ</code>,维度数组 <code>p[0..k]</code> 满足 <code>Aᵢ</code> 大小为 <code>p[i-1] × p[i]</code>。目标:只改变乘法<strong>括号化顺序</strong>,最小化标量乘法次数。</p>
<h4>递推与 DP 表</h4>
<p><code>m[i][j]</code> 表示从 <code>Aᵢ…Aⱼ</code> 的最小乘法次数1-index。则</p>
<section>
<eqn> m[i][i] = 0 </eqn>
</section>
<section>
<eqn> m[i][j] = min_{i ≤ k < j} ( 尝试推导一下这里的转移式 ) </eqn>
</section>
<h4>记录断点</h4>
<p><code>s[i][j]</code> 存储从 <code>Aᵢ…Aⱼ</code> 的最优断点。
<eq>$ s[i][j] = k \text{ if } m[i][j] == 与上面的式子一样 $</eq></p>
<h4>代码任务</h4>
<pre><code class="language-python">def matrix_chain_order(p: list[int]) -&gt; tuple[list[list[int]], list[list[int]]]:
&quot;&quot;&quot;
返回 (m, s)m[i][j] 为最小代价s[i][j] 为最优断点。
TODO: 长度 n = len(p)-1按区间长度 L=2..n 填表。
&quot;&quot;&quot;
...
def print_optimal_parens(s: list[list[int]], i: int, j: int) -&gt; str:
&quot;&quot;&quot;根据断点矩阵 s 输出最优括号化方案&quot;&quot;&quot;
if i == j:
return f&quot;A{i}&quot;
else:
return f&quot;({print_optimal_parens(s, i, s[i][j])}&quot; \
f&quot;{print_optimal_parens(s, s[i][j]+1, j)})&quot;
#测试验证部分
if __name__ == &quot;__main__&quot;:
# 经典测试案例
p = [30, 35, 15, 5, 10, 20, 25]
expected_result = 15125
# 计算最优解
m, s = matrix_chain_order(p)
n = len(p) - 1
result = m[1][n]
# 输出测试结果
print(f&quot;矩阵维度数组: {p}&quot;)
print(f&quot;矩阵数量: {n}&quot;)
print(f&quot;计算得到的最小标量乘法次数: {result}&quot;)
print(f&quot;预期的最小标量乘法次数: {expected_result}&quot;)
print(f&quot;测试{'通过' if result == expected_result else '失败'}&quot;)
print(f&quot;最优括号化方案: {print_optimal_parens(s, 1, n)}&quot;)
# 额外测试案例
p2 = [40, 20, 30, 10, 30]
m2, s2 = matrix_chain_order(p2)
print(&quot;\n第二个测试案例:&quot;)
print(f&quot;矩阵维度数组: {p2}&quot;)
print(f&quot;最小标量乘法次数: {m2[1][4]}&quot;) # 预期结果为 26000
print(f&quot;最优括号化方案: {print_optimal_parens(s2, 1, 4)}&quot;)
</code></pre>
<h4>综合讨论</h4>
<ul>
<li>何时选“记忆化 Top-Down”何时选“自底向上表格法”</li>
<li>如何从“纯递归”快速判断是否值得改造成 DP</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>分位统计量</h1>
<h2>分位统计量</h2>
<h3>第K大的数</h3>
<p>在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如:
• 一个数组中第 K 大的元素;
• 所有元素的中位数K = n/2
• 某个分位点25%、75%等)…
<br>
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 <eq>O(n \log n)</eq>,因为我们需要对所有元素进行排序。
<br>
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?
<br></p>
<h4>快速选择QuickSelect</h4>
<p><strong>快排中的“轴枢”启发</strong>
我们在快速排序中,每次选一个枢轴 pivot通过划分操作把数组分成两部分
• 左边元素 &lt;= pivot
• 右边元素 &gt;= pivot。
最终 pivot 会被放置固定位置上。
<br>
可见:</p>
<blockquote>
<p>若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。</p>
</blockquote>
<br>
因此可得快速选择的具体做法先令target等于N-K即找第N-K+1小的数其下标为target
1. 随机选一个枢轴;
2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx
3. 比较该位置与目标索引:
• 若 idx == target返回 pivot
• 若 idx > target在左半边递归查找
• 否则在右半边递归查找。
<br>
理解做法并分析时间复杂度。
### 快速选择的实现
<p>实现一个 <code>quickselect(arr, k)</code> 函数,返回第 K 大的元素
也就是增序列下标N-K位置的数</p>
<pre><code class="language-python">import random
def partition(arr, low, high):
&quot;&quot;&quot;
对arr序列从low到high下标找到一个枢轴并返回其下标。在arr中比枢轴小的数都在idx左边大的在右边
&quot;&quot;&quot;
def quickselect(arr, k):
&quot;&quot;&quot;
快速选择第K大元素转换为第n-k小
:param arr: 输入数组
:param k: 要找的第K大元素
:return: 该元素值
&quot;&quot;&quot;
n = len(arr)
target = n - k # 第k大转为第n-k小序列下标
low, high = 0, n - 1
while low &lt;= high:
idx = partition(arr, low, high)
if idx == target:
return arr[idx]
elif idx &lt; target:
low = idx + 1
else:
high = idx - 1
if __name__ == &quot;__main__&quot;:
#测试1随机数组
random.seed(42) # 设置随机种子,确保结果可重现
arr1 = [random.randint(1, 100) for _ in range(10)]
k1 = 3
result1 = quickselect(arr1.copy(), k1) # 使用副本避免修改原数组
sorted_arr1 = sorted(arr1)
expected1 = sorted_arr1[-k1]
print(f&quot;测试1 - 随机数组:&quot;)
print(f&quot;原数组: {arr1}&quot;)
print(f&quot;第{k1}大元素: {result1}, 预期值: {expected1}&quot;)
print(f&quot;测试{'通过' if result1 == expected1 else '失败'}\n&quot;)
#测试2包含重复元素的数组
arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
k2 = 2
result2 = quickselect(arr2.copy(), k2)
sorted_arr2 = sorted(arr2)
expected2 = sorted_arr2[-k2]
print(f&quot;测试2 - 包含重复元素:&quot;)
print(f&quot;原数组: {arr2}&quot;)
print(f&quot;第{k2}大元素: {result2}, 预期值: {expected2}&quot;)
print(f&quot;测试{'通过' if result2 == expected2 else '失败'}\n&quot;)
#测试3边界条件 - k=1最大元素
arr3 = [10, 20, 30, 40, 50]
k3 = 1
result3 = quickselect(arr3.copy(), k3)
sorted_arr3 = sorted(arr3)
expected3 = sorted_arr3[-k3]
print(f&quot;测试3 - 最大元素:&quot;)
print(f&quot;原数组: {arr3}&quot;)
print(f&quot;第{k3}大元素: {result3}, 预期值: {expected3}&quot;)
print(f&quot;测试{'通过' if result3 == expected3 else '失败'}\n&quot;)
</code></pre>
<h3>BFPRT 算法:中位数的中位数算法</h3>
<p>快速选择的平均复杂度为 <eq>O(n)</eq>,但可能退化为 <eq>O(n^2)</eq></p>
<h4>BFPRT分治法</h4>
<p>BFPRT算法和二分查找中对“矩阵”进行分治的做法很像也是通过矩阵的中心数来排除一部分数据。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" />
<br>
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:
<br></p>
<ol>
<li>将原始数组划分为若干个 5 个元素的组
请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。
组的个数 ≈ n / 5设 n 是总元素数量)
不足 5 个的组,可以用一个全局最小值(如 -1补全。
<br></li>
<li>对每组内部排序,取出中位数
图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。
每列上两个数小于中位数,下两个大于。
<br></li>
<li>将所有组的中位数组成新数组,对其递归调用本算法找中位数。
图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。
对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
<br></li>
<li>这个中位数(棕色圆点)就是我们分治的“枢轴”
有如下重要性质:
a. 棕点及左上角(图中蓝框的点)均小于等于枢轴
b. 棕点及右下角(图中黑框的点)均大于等于枢轴
<br></li>
<li>进行排除分解先令x=n-k+1找第k大数就是找第x小数
a. 若x&gt;蓝框数数量则在非蓝框数中找第k大
b. 若k&gt;黑框数数量则在非黑框数中找第k-黑框数数量)大
c. 当k=1或n'n'是当前子问题的数数量或n'&lt;=5均可在不大于O(n')的用时完成找数。
可以证明在c不成立的情况下a,b两点至少成立一个。
<br></li>
</ol>
<p>与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。</p>
<h3>BFPRT 复杂度分析</h3>
<p>在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的<strong>时间复杂度</strong>,并证明该算法的整体运行时间为 <eq>O(n)</eq></p>
<hr />
<h4>写出递推式</h4>
<p>根据 BFPRT 算法的执行过程依次与AI教师讨论每步的时间复杂度
<br></p>
<ol>
<li><strong><eq>n</eq> 个元素分组、找出每组中位数</strong>
<ul>
<li>分成 <eq>n/5</eq> 组,每组排序并取中位数。
<br></li>
</ul>
</li>
<li><strong>递归地找所有中位数的中位数(枢轴)</strong>
<ul>
<li>子问题规模为 <eq>n/5</eq>
<br></li>
</ul>
</li>
<li><strong>额外一次线性 partition</strong>
<ul>
<li>对全数组按枢轴划分:蓝框部分、黑框部分,此外递归时是对非蓝框或非黑框的补集部分。
<br></li>
</ul>
</li>
<li><strong>将原始数组按枢轴划分,并在一侧继续递归</strong>
<ul>
<li>对非蓝框或非黑框数进行递归查询。
<br></li>
</ul>
</li>
</ol>
<p><img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" /></p>
<p>从而得到最终的递推式。</p>
<hr />
<h4>证明 <eq>T(n) = O(n)</eq></h4>
<p>这里的证明并不复杂,使用高中学习的数学归纳法即可。
提示先假设满足T(n)&lt;=cn然后证明递推式&lt;=d*cn&lt;=cn其中d小于1。</p>
<p>与AI教师探讨证明流程。</p>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@@ -105,29 +105,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('chatbox').appendChild(requestMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}
// for (let i = 0; i < data.length; i++) {
// const element = data[i]
// const requestMessage = document.createElement('div');
// requestMessage.className = 'chat-message server-message';
// const approveButton = document.createElement('button');
// approveButton.innerText = '批准';
// approveButton.data = element;
// approveButton.style.width = '100%';
// approveButton.style.borderRadius = '4px';
// approveButton.onclick = function(event) {
// console.log(element)
// socket.emit('message', JSON.stringify({type:'function', data:element}));
// event.currentTarget.disabled = true;
// event.currentTarget.style.backgroundColor = 'gray';
// event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示
// event.currentTarget.style.border = 'none'; // 可选:去掉边框
// };
// // 将按钮添加到消息气泡中
// requestMessage.innerHTML = `<b>Function:</b> ${element.name}(${JSON.stringify(element.arguments)})<br>`;
// requestMessage.appendChild(approveButton);
// document.getElementById('chatbox').appendChild(requestMessage);
// document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
// }
} catch (error) {
console.log(error)
}
@@ -250,124 +228,9 @@ function sendMessage() {
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}
// let voice_stream = null;
// let audio_chunks = [];
// let isRecording = false;
// let recordingInterval = null;
// //鼠标点击终止事件
// function handleMouseClick(event) {
// event.stopPropagation();
// stopRecording();
// }
// //发送语音
// const startAudioRecording = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// audio: {
// sampleRate: 16000,
// channelCount: 1,
// echoCancellation: true,
// noiseSuppression: true
// }
// });
// const mediaRecorder = new MediaRecorder(stream);
// const audioContext = new AudioContext({
// sampleRate: 16000
// });
// // Load the AudioWorklet
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
// const source = audioContext.createMediaStreamSource(stream);
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
// let audioChunks = [];
// workletNode.port.onmessage = (e) => {
// audioChunks.push(new Float32Array(e.data));
// if (audioChunks.length >= 16) {
// // 合并所有音频块
// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
// const mergedData = new Float32Array(totalLength);
// let offset = 0;
// for (const chunk of audioChunks) {
// mergedData.set(chunk, offset);
// offset += chunk.length;
// }
// const audioMessage = {
// id: window.data.user_uuid,
// type: 'audio',
// data: mergedData,
// sampleRate: 16000,
// timestamp: Date.now()
// };
// socket.emit('voice', audioMessage);
// console.log("voice sended")
// audioChunks = [];
// }
// };
// source.connect(workletNode);
// workletNode.connect(audioContext.destination);
// mediaRecorder.value = {
// stop: () => {
// source.disconnect();
// workletNode.disconnect();
// stream.getTracks().forEach(track => track.stop());
// audioContext.close();
// }
// }
// isRecording.value = true;
// } catch (error) {
// console.error('Error starting audio recording:', error);
// }
// };
// function sendVoiceMessage() {
// startAudioRecording()
// //变换录音中图标
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
// document.addEventListener('click', handleMouseClick);
// }
// //停止录音
// function stopRecording() {
// if (!isRecording) return;
// isRecording = false;
// //取消监听
// document.removeEventListener('click', handleMouseClick);
// //取消定时器
// if (recordingInterval) {
// clearInterval(recordingInterval);
// recordingInterval = null;
// }
// //变换图标
// document.getElementById('recordIcon').className = 'bi bi-mic';
// //发送剩下的数据
// if (audio_chunks.length > 0) {
// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
// let offset = 0;
// for (const chunk of audio_chunks) {
// merged_data.set(new Uint8Array(chunk), offset);
// offset += chunk.length;
// }
// socket.emit('VoiceMessage', merged_data, { binary: true });
// audio_chunks = [];
// }
// overlay.style.display = 'none';
// //关闭音频流
// if (voice_stream) {
// voice_stream.stop_stream();
// voice_stream.close();
// voice_stream = null;
// }
// if (p) {
// p.terminate();
// p = null;
// }
// }
//语音这一块//

View File

@@ -28,4 +28,5 @@ region = ap-guangzhou
[ASE_ENGINE]
url = https://asengine.net
namespace = /socket.io/c159a41d238b00f35ff33e036d007a0dec4d03197084b5557949a2f7f5ffce03
namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1

View File

@@ -0,0 +1 @@
{"username": "_10235101550"}

View File

@@ -0,0 +1 @@
{"username": "a10235101450"}

View File

@@ -0,0 +1 @@
{"username": "ddd"}

View File

@@ -0,0 +1 @@
{"username": "dvd"}

View File

@@ -0,0 +1 @@
{"username": "emmeta"}

View File

@@ -0,0 +1 @@
{"username": "exceptedgoat"}

View File

@@ -0,0 +1 @@
{"username": "huo"}

View File

@@ -0,0 +1 @@
{"username": "jqs"}

View File

@@ -0,0 +1 @@
{"username": "kktq"}

View File

@@ -0,0 +1 @@
{"username": "luyu"}

View File

@@ -0,0 +1 @@
{"username": "not_shy_10235101481"}

View File

@@ -0,0 +1 @@
{"username": "sjm666"}

View File

@@ -0,0 +1 @@
{"username": "tian"}

View File

@@ -0,0 +1 @@
{"username": "wawacai"}

View File

@@ -0,0 +1 @@
{"username": "yy"}